@verifik/mcp 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +230 -2
- package/lib/catalog.js +166 -0
- package/lib/config.js +73 -0
- package/lib/filters.js +46 -0
- package/lib/meta-tools.js +125 -0
- package/lib/proxy.js +104 -0
- package/lib/tool-schema.js +164 -0
- package/lib/url-builder.js +58 -0
- package/package.json +44 -5
- package/server.js +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open-Verifik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @verifik/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Thin [Model Context Protocol](https://modelcontextprotocol.io) adapter for Verifik SmartCheck / Database Screening endpoints.
|
|
4
|
+
|
|
5
|
+
Each MCP tool maps 1:1 to an `AppFeature` from `GET /v2/app-features/my-list`. Tool calls proxy to the existing Verifik REST API with your API token, so auth, ClientFeature gating, and credit deduction stay unchanged.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart LR
|
|
11
|
+
A[MCP Client\nCursor / Claude Desktop] -->|stdio JSON-RPC| B["@verifik/mcp"]
|
|
12
|
+
B -->|GET /v2/app-features/my-list| C[Verifik API]
|
|
13
|
+
B -->|Bearer token proxy| C
|
|
14
|
+
C --> D[Existing middleware chain\nvalidateClient + billing]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Data flow:
|
|
18
|
+
|
|
19
|
+
1. On boot, the server loads the client catalog from `GET /v2/app-features/my-list`.
|
|
20
|
+
2. Eligible features become MCP tools (name derived from `code`, schema from `dependencies[]`).
|
|
21
|
+
3. `tools/call` proxies to `${VERIFIK_API_BASE}/${feature.url}` using the feature `method`.
|
|
22
|
+
4. Credits are charged by the normal API path — this server does not implement billing.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js 18+
|
|
27
|
+
- A Verifik **API token** from the Smart-Agent **API Tokens** screen (`/settings/api-key`)
|
|
28
|
+
|
|
29
|
+
API tokens are client JWTs minted by `POST /v2/auth/renew-and-revoke` while you are logged in. They include `clientId` and `JWTPhrase`, so they work for the catalog call and for feature endpoints the MCP proxies.
|
|
30
|
+
|
|
31
|
+
## Install (recommended)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @verifik/mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
No global install required. MCP clients invoke the package via `npx` (see configuration below).
|
|
38
|
+
|
|
39
|
+
### Local development fallback
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd verifik-mcp
|
|
43
|
+
npm install
|
|
44
|
+
node server.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment variables
|
|
48
|
+
|
|
49
|
+
| Variable | Required | Default | Description |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| `VERIFIK_API_TOKEN` | **Yes** | — | API token / client JWT from Smart-Agent |
|
|
52
|
+
| `VERIFIK_API_BASE` | No | `https://api.verifik.co` | API base URL (use `https://staging-api.verifik.co` for staging) |
|
|
53
|
+
| `VERIFIK_MCP_SMARTCHECK_ONLY` | No | `true` | Only expose `smartCheckEnabled` features |
|
|
54
|
+
| `VERIFIK_MCP_COUNTRY` | No | — | Comma-separated country filter (e.g. `Colombia,world`) |
|
|
55
|
+
| `VERIFIK_MCP_BASE_CATEGORY` | No | — | Comma-separated `baseCategory` filter |
|
|
56
|
+
| `VERIFIK_MCP_CODES` | No | — | Comma-separated feature code allowlist |
|
|
57
|
+
| `VERIFIK_MCP_CATALOG_REFRESH_MS` | No | `0` | Optional in-memory catalog refresh interval |
|
|
58
|
+
|
|
59
|
+
Security: keep the token in environment variables only. The server never logs the full JWT.
|
|
60
|
+
|
|
61
|
+
## Cursor configuration (`mcp.json`)
|
|
62
|
+
|
|
63
|
+
Add to your Cursor MCP settings (global or project-level):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"verifik-smartcheck": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "@verifik/mcp"],
|
|
71
|
+
"env": {
|
|
72
|
+
"VERIFIK_API_TOKEN": "YOUR_API_TOKEN",
|
|
73
|
+
"VERIFIK_API_BASE": "https://api.verifik.co",
|
|
74
|
+
"VERIFIK_MCP_SMARTCHECK_ONLY": "true",
|
|
75
|
+
"VERIFIK_MCP_COUNTRY": "Colombia,world"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Claude Desktop configuration
|
|
83
|
+
|
|
84
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent Claude Desktop config file:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"verifik-smartcheck": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "@verifik/mcp"],
|
|
92
|
+
"env": {
|
|
93
|
+
"VERIFIK_API_TOKEN": "YOUR_API_TOKEN",
|
|
94
|
+
"VERIFIK_API_BASE": "https://api.verifik.co"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Generic MCP JSON (other clients)
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"verifik-smartcheck": {
|
|
107
|
+
"command": "npx",
|
|
108
|
+
"args": ["-y", "@verifik/mcp"],
|
|
109
|
+
"env": {
|
|
110
|
+
"VERIFIK_API_TOKEN": "YOUR_API_TOKEN",
|
|
111
|
+
"VERIFIK_API_BASE": "https://api.verifik.co",
|
|
112
|
+
"VERIFIK_MCP_SMARTCHECK_ONLY": "true"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Local repo fallback (no npm publish yet)
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"verifik-smartcheck": {
|
|
125
|
+
"command": "node",
|
|
126
|
+
"args": ["/absolute/path/to/verifik-mcp/server.js"],
|
|
127
|
+
"env": {
|
|
128
|
+
"VERIFIK_API_TOKEN": "YOUR_API_TOKEN",
|
|
129
|
+
"VERIFIK_API_BASE": "https://api.verifik.co"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Restart Cursor / Claude Desktop after editing MCP config.
|
|
137
|
+
|
|
138
|
+
## Tools
|
|
139
|
+
|
|
140
|
+
### Meta tools (no credit charge)
|
|
141
|
+
|
|
142
|
+
- `verifik_list_catalog` — filtered feature summaries from the in-memory catalog
|
|
143
|
+
- `verifik_get_feature` — full metadata + dependency JSON schema for one `code`
|
|
144
|
+
|
|
145
|
+
### Feature tools
|
|
146
|
+
|
|
147
|
+
One tool per eligible `AppFeature`:
|
|
148
|
+
|
|
149
|
+
- **Name:** sanitized `code` (`A-Za-z0-9._-`, max 128 chars)
|
|
150
|
+
- **Description:** name, country, description, credit note
|
|
151
|
+
- **Input schema:** built from `dependencies[]`
|
|
152
|
+
- `String` → `string`
|
|
153
|
+
- `Number` / `Integer` → `number`
|
|
154
|
+
- `Boolean` → `boolean`
|
|
155
|
+
- `enum`, `min`, `max`, `description`, `required`
|
|
156
|
+
- **Call behavior:** HTTP proxy to the feature `url` using `method` (`GET` query params by default)
|
|
157
|
+
|
|
158
|
+
Features with binary/file dependencies (images, selfies, uploads) are skipped until a safe transport exists.
|
|
159
|
+
|
|
160
|
+
## Smoke test
|
|
161
|
+
|
|
162
|
+
### 1. Unit tests (no live token)
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd verifik-mcp
|
|
166
|
+
npm test
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 2. Missing token exits cleanly
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
node server.js
|
|
173
|
+
# stderr: VERIFIK_API_TOKEN is required (client JWT / API bearer token)
|
|
174
|
+
# exit code: 1
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3. Live catalog load (requires token)
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
export VERIFIK_API_TOKEN="YOUR_API_TOKEN"
|
|
181
|
+
node -e "
|
|
182
|
+
const { loadConfig } = require('./lib/config');
|
|
183
|
+
const { loadCatalogState } = require('./lib/catalog');
|
|
184
|
+
(async () => {
|
|
185
|
+
const config = loadConfig();
|
|
186
|
+
const state = await loadCatalogState(config);
|
|
187
|
+
console.log('tools:', state.tools.length);
|
|
188
|
+
console.log('sample:', state.tools.slice(0, 3).map(t => t.name));
|
|
189
|
+
})().catch(err => { console.error(err.message); process.exit(1); });
|
|
190
|
+
"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 4. MCP stdio handshake
|
|
194
|
+
|
|
195
|
+
Start the server via Cursor MCP panel, or use any MCP client that supports stdio transport.
|
|
196
|
+
|
|
197
|
+
## Example tool call result
|
|
198
|
+
|
|
199
|
+
Non-2xx responses are returned as MCP text with HTTP status and JSON body so agents can handle `404`, `409`, and `402` the same as REST:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"httpStatus": 409,
|
|
204
|
+
"statusText": "Conflict",
|
|
205
|
+
"durationMs": 142,
|
|
206
|
+
"request": {
|
|
207
|
+
"method": "GET",
|
|
208
|
+
"url": "https://api.verifik.co/v2/co/cedula?documentType=CC"
|
|
209
|
+
},
|
|
210
|
+
"body": {
|
|
211
|
+
"code": "MissingParameter",
|
|
212
|
+
"message": "documentNumber is required"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Publishing
|
|
218
|
+
|
|
219
|
+
Published as [`@verifik/mcp`](https://www.npmjs.com/package/@verifik/mcp) from this directory.
|
|
220
|
+
|
|
221
|
+
Release tags use the `v*` prefix (for example `v0.1.0`). See the repository workflow `.github/workflows/publish.yml`.
|
|
222
|
+
|
|
223
|
+
## Follow-ups
|
|
224
|
+
|
|
225
|
+
- Streamable HTTP MCP transport for hosted deployments
|
|
226
|
+
- Biometric / multipart endpoints once file upload bridging is defined
|
|
227
|
+
|
|
228
|
+
## Related
|
|
229
|
+
|
|
230
|
+
- GitHub issue: https://github.com/Open-Verifik/verifik-backend/issues/370
|
|
231
|
+
- Support ticket: #1313
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { filterFeatures } = require("./filters");
|
|
4
|
+
const { buildToolRegistry, toMcpToolDefinition } = require("./tool-schema");
|
|
5
|
+
|
|
6
|
+
const CATALOG_PATH = "/v2/app-features/my-list";
|
|
7
|
+
const PAGE_LIMIT = 500;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import("./types").McpConfig} config
|
|
11
|
+
*/
|
|
12
|
+
const fetchCatalogPage = async (config, page = 1) => {
|
|
13
|
+
const url = new URL(`${config.apiBase}${CATALOG_PATH}`);
|
|
14
|
+
url.searchParams.set("page", String(page));
|
|
15
|
+
url.searchParams.set("limit", String(PAGE_LIMIT));
|
|
16
|
+
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${config.token}`,
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const body = await readResponseBody(response);
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Catalog request failed (${response.status}): ${formatBodyForError(body)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return normalizeCatalogResponse(body);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {unknown} body
|
|
36
|
+
*/
|
|
37
|
+
const normalizeCatalogResponse = (body) => {
|
|
38
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
39
|
+
const data = Array.isArray(payload.data)
|
|
40
|
+
? payload.data
|
|
41
|
+
: Array.isArray(payload.data?.docs)
|
|
42
|
+
? payload.data.docs
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
features: data,
|
|
47
|
+
page: Number(payload.page) || 1,
|
|
48
|
+
pages: Number(payload.pages) || 1,
|
|
49
|
+
total: Number(payload.total) || data.length,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {import("./types").McpConfig} config
|
|
55
|
+
*/
|
|
56
|
+
const fetchAllCatalogFeatures = async (config) => {
|
|
57
|
+
const firstPage = await fetchCatalogPage(config, 1);
|
|
58
|
+
const features = [...firstPage.features];
|
|
59
|
+
|
|
60
|
+
for (let page = firstPage.page + 1; page <= firstPage.pages; page += 1) {
|
|
61
|
+
const nextPage = await fetchCatalogPage(config, page);
|
|
62
|
+
features.push(...nextPage.features);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return features;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {import("./types").McpConfig} config
|
|
70
|
+
*/
|
|
71
|
+
const loadCatalogState = async (config) => {
|
|
72
|
+
const allFeatures = await fetchAllCatalogFeatures(config);
|
|
73
|
+
const filteredFeatures = filterFeatures(allFeatures, config);
|
|
74
|
+
const registry = buildToolRegistry(filteredFeatures);
|
|
75
|
+
|
|
76
|
+
const tools = [];
|
|
77
|
+
|
|
78
|
+
for (const [toolName, feature] of registry.toolNameToFeature.entries()) {
|
|
79
|
+
tools.push(toMcpToolDefinition(feature, toolName));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
features: filteredFeatures,
|
|
84
|
+
allFeatures,
|
|
85
|
+
tools,
|
|
86
|
+
registry,
|
|
87
|
+
loadedAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {Response} response
|
|
93
|
+
*/
|
|
94
|
+
const readResponseBody = async (response) => {
|
|
95
|
+
const contentType = response.headers.get("content-type") || "";
|
|
96
|
+
|
|
97
|
+
if (contentType.includes("application/json")) {
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return response.text();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {unknown} body
|
|
106
|
+
*/
|
|
107
|
+
const formatBodyForError = (body) => {
|
|
108
|
+
if (typeof body === "string") return body.slice(0, 500);
|
|
109
|
+
try {
|
|
110
|
+
return JSON.stringify(body).slice(0, 500);
|
|
111
|
+
} catch {
|
|
112
|
+
return "unknown_error";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
class CatalogCache {
|
|
117
|
+
/**
|
|
118
|
+
* @param {import("./types").McpConfig} config
|
|
119
|
+
*/
|
|
120
|
+
constructor(config) {
|
|
121
|
+
this.config = config;
|
|
122
|
+
this.state = null;
|
|
123
|
+
this.loadingPromise = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getState(forceRefresh = false) {
|
|
127
|
+
const shouldRefresh =
|
|
128
|
+
forceRefresh ||
|
|
129
|
+
!this.state ||
|
|
130
|
+
(this.config.catalogRefreshMs > 0 &&
|
|
131
|
+
Date.now() - Date.parse(this.state.loadedAt) >= this.config.catalogRefreshMs);
|
|
132
|
+
|
|
133
|
+
if (!shouldRefresh) return this.state;
|
|
134
|
+
if (this.loadingPromise) return this.loadingPromise;
|
|
135
|
+
|
|
136
|
+
this.loadingPromise = loadCatalogState(this.config)
|
|
137
|
+
.then((state) => {
|
|
138
|
+
this.state = state;
|
|
139
|
+
return state;
|
|
140
|
+
})
|
|
141
|
+
.finally(() => {
|
|
142
|
+
this.loadingPromise = null;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return this.loadingPromise;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
findFeatureByCode(code) {
|
|
149
|
+
if (!this.state) return null;
|
|
150
|
+
return this.state.features.find((feature) => feature.code === code) || null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resolveFeatureByToolName(toolName) {
|
|
154
|
+
if (!this.state) return null;
|
|
155
|
+
return this.state.registry.toolNameToFeature.get(toolName) || null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
CATALOG_PATH,
|
|
161
|
+
fetchCatalogPage,
|
|
162
|
+
fetchAllCatalogFeatures,
|
|
163
|
+
loadCatalogState,
|
|
164
|
+
CatalogCache,
|
|
165
|
+
normalizeCatalogResponse,
|
|
166
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = "https://api.verifik.co";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @returns {import("./types").McpConfig}
|
|
7
|
+
*/
|
|
8
|
+
const loadConfig = () => {
|
|
9
|
+
const apiBase = String(process.env.VERIFIK_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
10
|
+
const token = String(process.env.VERIFIK_API_TOKEN || "").trim();
|
|
11
|
+
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error("VERIFIK_API_TOKEN is required (client JWT / API bearer token)");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const smartCheckOnly = parseBooleanEnv(process.env.VERIFIK_MCP_SMARTCHECK_ONLY, true);
|
|
17
|
+
const countryFilter = parseListEnv(process.env.VERIFIK_MCP_COUNTRY);
|
|
18
|
+
const baseCategoryFilter = parseListEnv(process.env.VERIFIK_MCP_BASE_CATEGORY);
|
|
19
|
+
const codeAllowlist = parseListEnv(process.env.VERIFIK_MCP_CODES);
|
|
20
|
+
const catalogRefreshMs = parsePositiveInt(process.env.VERIFIK_MCP_CATALOG_REFRESH_MS, 0);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
apiBase,
|
|
24
|
+
token,
|
|
25
|
+
smartCheckOnly,
|
|
26
|
+
countryFilter,
|
|
27
|
+
baseCategoryFilter,
|
|
28
|
+
codeAllowlist,
|
|
29
|
+
catalogRefreshMs,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string|undefined} value
|
|
35
|
+
* @param {boolean} defaultValue
|
|
36
|
+
*/
|
|
37
|
+
const parseBooleanEnv = (value, defaultValue) => {
|
|
38
|
+
if (value == null || value === "") return defaultValue;
|
|
39
|
+
const normalized = String(value).trim().toLowerCase();
|
|
40
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
41
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
42
|
+
return defaultValue;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string|undefined} value
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
const parseListEnv = (value) => {
|
|
50
|
+
if (!value || !String(value).trim()) return [];
|
|
51
|
+
return String(value)
|
|
52
|
+
.split(",")
|
|
53
|
+
.map((entry) => entry.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string|undefined} value
|
|
59
|
+
* @param {number} defaultValue
|
|
60
|
+
*/
|
|
61
|
+
const parsePositiveInt = (value, defaultValue) => {
|
|
62
|
+
if (value == null || value === "") return defaultValue;
|
|
63
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
64
|
+
if (!Number.isFinite(parsed) || parsed < 0) return defaultValue;
|
|
65
|
+
return parsed;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
DEFAULT_API_BASE,
|
|
70
|
+
loadConfig,
|
|
71
|
+
parseBooleanEnv,
|
|
72
|
+
parseListEnv,
|
|
73
|
+
};
|
package/lib/filters.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import("./types").AppFeature} feature
|
|
5
|
+
* @param {import("./types").McpConfig} config
|
|
6
|
+
*/
|
|
7
|
+
const isEligibleFeature = (feature, config) => {
|
|
8
|
+
if (!feature?.code || !feature?.url) return false;
|
|
9
|
+
if (config.smartCheckOnly && !feature.smartCheckEnabled) return false;
|
|
10
|
+
if (config.codeAllowlist.length && !config.codeAllowlist.includes(feature.code)) return false;
|
|
11
|
+
if (config.countryFilter.length && !config.countryFilter.includes(feature.country)) return false;
|
|
12
|
+
if (config.baseCategoryFilter.length && !config.baseCategoryFilter.includes(feature.baseCategory)) return false;
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import("./types").AppFeature[]} features
|
|
18
|
+
* @param {import("./types").McpConfig} config
|
|
19
|
+
*/
|
|
20
|
+
const filterFeatures = (features, config) => {
|
|
21
|
+
if (!Array.isArray(features)) return [];
|
|
22
|
+
return features.filter((feature) => isEligibleFeature(feature, config));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import("./types").AppFeature} feature
|
|
27
|
+
*/
|
|
28
|
+
const toFeatureSummary = (feature) => ({
|
|
29
|
+
code: feature.code,
|
|
30
|
+
name: feature.name,
|
|
31
|
+
description: feature.description,
|
|
32
|
+
country: feature.country,
|
|
33
|
+
baseCategory: feature.baseCategory,
|
|
34
|
+
url: feature.url,
|
|
35
|
+
method: feature.method || "GET",
|
|
36
|
+
smartCheckEnabled: Boolean(feature.smartCheckEnabled),
|
|
37
|
+
price: feature.price,
|
|
38
|
+
smartCheckPrice: feature.smartCheckPrice,
|
|
39
|
+
dependencyCount: Array.isArray(feature.dependencies) ? feature.dependencies.length : 0,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
isEligibleFeature,
|
|
44
|
+
filterFeatures,
|
|
45
|
+
toFeatureSummary,
|
|
46
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { toFeatureSummary } = require("./filters");
|
|
4
|
+
const { buildInputSchema } = require("./tool-schema");
|
|
5
|
+
|
|
6
|
+
const LIST_CATALOG_TOOL = "verifik_list_catalog";
|
|
7
|
+
const GET_FEATURE_TOOL = "verifik_get_feature";
|
|
8
|
+
|
|
9
|
+
const META_TOOL_DEFINITIONS = [
|
|
10
|
+
{
|
|
11
|
+
name: LIST_CATALOG_TOOL,
|
|
12
|
+
description:
|
|
13
|
+
"List SmartCheck / Database Screening features available to this client without charging credits.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
country: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Optional comma-separated country filter (e.g. Colombia,world).",
|
|
20
|
+
},
|
|
21
|
+
baseCategory: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Optional comma-separated baseCategory filter.",
|
|
24
|
+
},
|
|
25
|
+
code: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional comma-separated feature code allowlist.",
|
|
28
|
+
},
|
|
29
|
+
smartCheckOnly: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
description: "When true, only return smartCheckEnabled features. Defaults to server config.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: GET_FEATURE_TOOL,
|
|
39
|
+
description: "Get full metadata and dependency schema for one AppFeature by code (no credit charge).",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
code: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "AppFeature code (e.g. colombia_api_identity_lookup).",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["code"],
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const META_TOOL_NAMES = new Set(META_TOOL_DEFINITIONS.map((tool) => tool.name));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import("./catalog").CatalogCache} catalogCache
|
|
58
|
+
* @param {Record<string, unknown>} args
|
|
59
|
+
*/
|
|
60
|
+
const handleListCatalog = async (catalogCache, args = {}) => {
|
|
61
|
+
const state = await catalogCache.getState();
|
|
62
|
+
const countryFilter = parseOptionalList(args.country);
|
|
63
|
+
const baseCategoryFilter = parseOptionalList(args.baseCategory);
|
|
64
|
+
const codeFilter = parseOptionalList(args.code);
|
|
65
|
+
const smartCheckOnly =
|
|
66
|
+
args.smartCheckOnly === undefined ? undefined : Boolean(args.smartCheckOnly);
|
|
67
|
+
|
|
68
|
+
const features = state.features.filter((feature) => {
|
|
69
|
+
if (smartCheckOnly === true && !feature.smartCheckEnabled) return false;
|
|
70
|
+
if (countryFilter.length && !countryFilter.includes(feature.country)) return false;
|
|
71
|
+
if (baseCategoryFilter.length && !baseCategoryFilter.includes(feature.baseCategory)) return false;
|
|
72
|
+
if (codeFilter.length && !codeFilter.includes(feature.code)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
loadedAt: state.loadedAt,
|
|
78
|
+
total: features.length,
|
|
79
|
+
features: features.map(toFeatureSummary),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {import("./catalog").CatalogCache} catalogCache
|
|
85
|
+
* @param {Record<string, unknown>} args
|
|
86
|
+
*/
|
|
87
|
+
const handleGetFeature = async (catalogCache, args = {}) => {
|
|
88
|
+
const code = String(args.code || "").trim();
|
|
89
|
+
|
|
90
|
+
if (!code) {
|
|
91
|
+
throw new Error("code is required");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const feature = catalogCache.findFeatureByCode(code);
|
|
95
|
+
|
|
96
|
+
if (!feature) {
|
|
97
|
+
throw new Error(`feature_not_found:${code}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...toFeatureSummary(feature),
|
|
102
|
+
dependencies: feature.dependencies || [],
|
|
103
|
+
inputSchema: buildInputSchema(feature),
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {unknown} value
|
|
109
|
+
*/
|
|
110
|
+
const parseOptionalList = (value) => {
|
|
111
|
+
if (value == null || value === "") return [];
|
|
112
|
+
return String(value)
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((entry) => entry.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
LIST_CATALOG_TOOL,
|
|
120
|
+
GET_FEATURE_TOOL,
|
|
121
|
+
META_TOOL_DEFINITIONS,
|
|
122
|
+
META_TOOL_NAMES,
|
|
123
|
+
handleListCatalog,
|
|
124
|
+
handleGetFeature,
|
|
125
|
+
};
|
package/lib/proxy.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
buildFeatureRequestUrl,
|
|
5
|
+
normalizeHttpMethod,
|
|
6
|
+
sanitizeRequestParams,
|
|
7
|
+
splitRequestPayload,
|
|
8
|
+
} = require("./url-builder");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import("./types").AppFeature} feature
|
|
12
|
+
* @param {Record<string, unknown>} args
|
|
13
|
+
* @param {import("./types").McpConfig} config
|
|
14
|
+
*/
|
|
15
|
+
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);
|
|
19
|
+
const { query, body } = splitRequestPayload(method, params);
|
|
20
|
+
|
|
21
|
+
const requestInit = {
|
|
22
|
+
method,
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${config.token}`,
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (body !== undefined) {
|
|
30
|
+
requestInit.headers["Content-Type"] = "application/json";
|
|
31
|
+
requestInit.body = JSON.stringify(body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const requestUrl = query ? appendQueryParams(url, query) : url;
|
|
35
|
+
const startedAt = Date.now();
|
|
36
|
+
const response = await fetch(requestUrl, requestInit);
|
|
37
|
+
const responseBody = await readResponseBody(response);
|
|
38
|
+
const durationMs = Date.now() - startedAt;
|
|
39
|
+
|
|
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
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} url
|
|
55
|
+
* @param {Record<string, unknown>} query
|
|
56
|
+
*/
|
|
57
|
+
const appendQueryParams = (url, query) => {
|
|
58
|
+
const target = new URL(url);
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(query)) {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
for (const entry of value) target.searchParams.append(key, String(entry));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
target.searchParams.set(key, String(value));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return target.toString();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Response} response
|
|
74
|
+
*/
|
|
75
|
+
const readResponseBody = async (response) => {
|
|
76
|
+
const contentType = response.headers.get("content-type") || "";
|
|
77
|
+
|
|
78
|
+
if (contentType.includes("application/json")) {
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response.text();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {{ status: number, statusText?: string, body: unknown, durationMs?: number, request?: { method: string, url: string } }} result
|
|
87
|
+
*/
|
|
88
|
+
const formatProxyResult = (result) => {
|
|
89
|
+
const payload = {
|
|
90
|
+
httpStatus: result.status,
|
|
91
|
+
statusText: result.statusText,
|
|
92
|
+
durationMs: result.durationMs,
|
|
93
|
+
request: result.request,
|
|
94
|
+
body: result.body,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return JSON.stringify(payload, null, 2);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
invokeFeature,
|
|
102
|
+
formatProxyResult,
|
|
103
|
+
appendQueryParams,
|
|
104
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createHash } = require("node:crypto");
|
|
4
|
+
|
|
5
|
+
const TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
|
|
6
|
+
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
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import("./types").AppFeatureDependency} dependency
|
|
30
|
+
*/
|
|
31
|
+
const mapDependencyType = (dependency) => {
|
|
32
|
+
const type = String(dependency?.type || "String").trim();
|
|
33
|
+
|
|
34
|
+
if (/^(number|integer|float|decimal)$/i.test(type)) return "number";
|
|
35
|
+
if (/^(boolean|bool)$/i.test(type)) return "boolean";
|
|
36
|
+
return "string";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import("./types").AppFeatureDependency} dependency
|
|
41
|
+
*/
|
|
42
|
+
const isBinaryDependency = (dependency) => {
|
|
43
|
+
const type = String(dependency?.type || "").trim();
|
|
44
|
+
const field = String(dependency?.field || "").trim();
|
|
45
|
+
|
|
46
|
+
if (BINARY_TYPE_PATTERN.test(type)) return true;
|
|
47
|
+
if (BINARY_FIELD_PATTERN.test(field) && !/documentType|documentNumber/i.test(field)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {import("./types").AppFeature} feature
|
|
53
|
+
*/
|
|
54
|
+
const hasUnsupportedDependencies = (feature) => {
|
|
55
|
+
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
56
|
+
return dependencies.some((dependency) => isBinaryDependency(dependency));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
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
|
+
/**
|
|
74
|
+
* @param {import("./types").AppFeatureDependency} dependency
|
|
75
|
+
*/
|
|
76
|
+
const buildDependencyProperty = (dependency) => {
|
|
77
|
+
const property = {
|
|
78
|
+
type: mapDependencyType(dependency),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (dependency.description) property.description = dependency.description;
|
|
82
|
+
if (dependency.default != null) property.default = dependency.default;
|
|
83
|
+
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;
|
|
86
|
+
|
|
87
|
+
return property;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {import("./types").AppFeature} feature
|
|
92
|
+
*/
|
|
93
|
+
const buildInputSchema = (feature) => {
|
|
94
|
+
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
95
|
+
const properties = {};
|
|
96
|
+
const required = [];
|
|
97
|
+
|
|
98
|
+
for (const dependency of dependencies) {
|
|
99
|
+
if (!dependency?.field || isBinaryDependency(dependency)) continue;
|
|
100
|
+
|
|
101
|
+
properties[dependency.field] = buildDependencyProperty(dependency);
|
|
102
|
+
|
|
103
|
+
if (dependency.required) required.push(dependency.field);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const schema = {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties,
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (required.length) schema.required = required;
|
|
113
|
+
|
|
114
|
+
return schema;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {import("./types").AppFeature[]} features
|
|
119
|
+
*/
|
|
120
|
+
const buildToolRegistry = (features) => {
|
|
121
|
+
const toolNameToFeature = new Map();
|
|
122
|
+
const featureCodeToToolName = new Map();
|
|
123
|
+
const usedNames = new Set();
|
|
124
|
+
|
|
125
|
+
for (const feature of features) {
|
|
126
|
+
if (!feature?.code || !feature?.url || hasUnsupportedDependencies(feature)) continue;
|
|
127
|
+
|
|
128
|
+
let toolName = sanitizeToolName(feature.code);
|
|
129
|
+
|
|
130
|
+
if (usedNames.has(toolName)) {
|
|
131
|
+
const suffix = createHash("sha1").update(feature.code).digest("hex").slice(0, 6);
|
|
132
|
+
toolName = sanitizeToolName(`${feature.code}_${suffix}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
usedNames.add(toolName);
|
|
136
|
+
toolNameToFeature.set(toolName, feature);
|
|
137
|
+
featureCodeToToolName.set(feature.code, toolName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
toolNameToFeature,
|
|
142
|
+
featureCodeToToolName,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {import("./types").AppFeature} feature
|
|
148
|
+
*/
|
|
149
|
+
const toMcpToolDefinition = (feature, toolName) => ({
|
|
150
|
+
name: toolName,
|
|
151
|
+
description: buildToolDescription(feature),
|
|
152
|
+
inputSchema: buildInputSchema(feature),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
sanitizeToolName,
|
|
157
|
+
mapDependencyType,
|
|
158
|
+
isBinaryDependency,
|
|
159
|
+
hasUnsupportedDependencies,
|
|
160
|
+
buildToolDescription,
|
|
161
|
+
buildInputSchema,
|
|
162
|
+
buildToolRegistry,
|
|
163
|
+
toMcpToolDefinition,
|
|
164
|
+
};
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"verifik-mcp": "./server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node server.js",
|
|
16
|
+
"test": "node --test tests/*.test.js",
|
|
17
|
+
"test:sandbox": "node --test tests/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"verifik",
|
|
25
|
+
"smartcheck",
|
|
26
|
+
"model-context-protocol"
|
|
27
|
+
],
|
|
28
|
+
"author": "Open-Verifik",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/Open-Verifik/verifik-mcp.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/Open-Verifik/verifik-mcp#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Open-Verifik/verifik-mcp/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
43
|
+
"zod": "^3.23.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
10
|
+
|
|
11
|
+
const { loadConfig } = require("./lib/config");
|
|
12
|
+
const { CatalogCache } = require("./lib/catalog");
|
|
13
|
+
const { invokeFeature, formatProxyResult } = require("./lib/proxy");
|
|
14
|
+
const {
|
|
15
|
+
META_TOOL_DEFINITIONS,
|
|
16
|
+
META_TOOL_NAMES,
|
|
17
|
+
handleListCatalog,
|
|
18
|
+
handleGetFeature,
|
|
19
|
+
} = require("./lib/meta-tools");
|
|
20
|
+
|
|
21
|
+
const { version: packageVersion } = require("./package.json");
|
|
22
|
+
|
|
23
|
+
const SERVER_NAME = "verifik-smartcheck";
|
|
24
|
+
const SERVER_VERSION = packageVersion;
|
|
25
|
+
|
|
26
|
+
const logStderr = (message) => {
|
|
27
|
+
process.stderr.write(`[${SERVER_NAME}] ${message}\n`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const createServer = async () => {
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
const catalogCache = new CatalogCache(config);
|
|
33
|
+
|
|
34
|
+
logStderr(`Loading catalog from ${config.apiBase} ...`);
|
|
35
|
+
await catalogCache.getState();
|
|
36
|
+
logStderr(`Catalog loaded with ${catalogCache.state.tools.length} proxy tools.`);
|
|
37
|
+
|
|
38
|
+
const server = new Server(
|
|
39
|
+
{
|
|
40
|
+
name: SERVER_NAME,
|
|
41
|
+
version: SERVER_VERSION,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
capabilities: {
|
|
45
|
+
tools: {},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
51
|
+
const state = await catalogCache.getState();
|
|
52
|
+
return {
|
|
53
|
+
tools: [...META_TOOL_DEFINITIONS, ...state.tools],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
58
|
+
const toolName = request.params.name;
|
|
59
|
+
const args = request.params.arguments || {};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (toolName === "verifik_list_catalog") {
|
|
63
|
+
const payload = await handleListCatalog(catalogCache, args);
|
|
64
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (toolName === "verifik_get_feature") {
|
|
68
|
+
const payload = await handleGetFeature(catalogCache, args);
|
|
69
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const feature = catalogCache.resolveFeatureByToolName(toolName);
|
|
73
|
+
|
|
74
|
+
if (!feature) {
|
|
75
|
+
return errorResult(`Unknown tool: ${toolName}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const proxyResult = await invokeFeature(feature, args, config);
|
|
79
|
+
return textResult(formatProxyResult(proxyResult), !proxyResult.ok);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return server;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} text
|
|
90
|
+
* @param {boolean} isError
|
|
91
|
+
*/
|
|
92
|
+
const textResult = (text, isError = false) => ({
|
|
93
|
+
content: [{ type: "text", text }],
|
|
94
|
+
isError,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} message
|
|
99
|
+
*/
|
|
100
|
+
const errorResult = (message) => textResult(message, true);
|
|
101
|
+
|
|
102
|
+
const main = async () => {
|
|
103
|
+
const server = await createServer();
|
|
104
|
+
const transport = new StdioServerTransport();
|
|
105
|
+
await server.connect(transport);
|
|
106
|
+
logStderr("stdio transport connected.");
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
main().catch((error) => {
|
|
110
|
+
logStderr(error instanceof Error ? error.message : String(error));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|