@klarefi/mcp 0.2.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/dist/tools.js ADDED
@@ -0,0 +1,450 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ import { Klarefi } from "@klarefi/node";
4
+ import { IntakeSessionRegistry } from "./sessions.js";
5
+ const MAX_INLINE_UPLOAD_BYTES = 5 * 1024 * 1024;
6
+ const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
7
+ export const tools = [
8
+ {
9
+ name: "create_connector_from_openapi",
10
+ description: "Import a public OpenAPI JSON URL as a Klarefi verification connector. " +
11
+ "JSON specs only (convert YAML first). The connector goes live " +
12
+ "immediately — review its functions before routing live traffic.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ connector_key: { type: "string" },
17
+ openapi_url: { type: "string" },
18
+ label: { type: "string" },
19
+ base_url: { type: "string" },
20
+ },
21
+ required: ["connector_key", "openapi_url"],
22
+ },
23
+ },
24
+ {
25
+ name: "create_connector",
26
+ description: "Create or update a Klarefi verification connector manifest. The " +
27
+ "connector is active as soon as this returns — there is no draft/review " +
28
+ "step, so review its functions before routing live traffic.",
29
+ inputSchema: {
30
+ type: "object",
31
+ additionalProperties: true,
32
+ required: [
33
+ "connector_key",
34
+ "label",
35
+ "connector_type",
36
+ "transport",
37
+ "functions",
38
+ ],
39
+ },
40
+ },
41
+ {
42
+ name: "delete_connector",
43
+ description: "Delete a configured Klarefi connector by key.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ connector_key: { type: "string" },
48
+ },
49
+ required: ["connector_key"],
50
+ },
51
+ },
52
+ {
53
+ name: "create_intake_session",
54
+ description: "Create a hosted Klarefi intake session.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ case_type_id: { type: "string" },
59
+ external_case_id: { type: "string" },
60
+ external_applicant_id: { type: "string" },
61
+ external_customer_id: { type: "string" },
62
+ return_url: { type: "string" },
63
+ ttl_hours: { type: "number" },
64
+ locale: { type: "string" },
65
+ prefill: {
66
+ type: "object",
67
+ additionalProperties: true,
68
+ description: "Prefill payload passed through to the session; schema owned by the prefill feature",
69
+ },
70
+ },
71
+ required: ["case_type_id"],
72
+ },
73
+ },
74
+ {
75
+ name: "get_case",
76
+ description: "Retrieve a Klarefi case and resolved facts.",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ case_id: { type: "string" },
81
+ },
82
+ required: ["case_id"],
83
+ },
84
+ },
85
+ {
86
+ name: "open_intake",
87
+ description: "Open an applicant-plane intake session from a signed URL or from session_id plus access_token. No KLAREFI_API_KEY is required.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ url: {
92
+ type: "string",
93
+ description: "Signed hosted intake URL.",
94
+ },
95
+ session_id: { type: "string" },
96
+ access_token: { type: "string" },
97
+ api_base_url: {
98
+ type: "string",
99
+ description: "Klarefi API base URL. Defaults to KLAREFI_API_BASE_URL or https://api.klarefi.com.",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ {
105
+ name: "get_intake_state",
106
+ description: "Get the current applicant-plane intake state for the selected or most recently opened session.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ session_id: { type: "string" },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ name: "answer_question",
116
+ description: "Answer the active applicant question with free text, optionally targeting a task_id.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ text: { type: "string" },
121
+ session_id: { type: "string" },
122
+ task_id: { type: "string" },
123
+ },
124
+ required: ["text"],
125
+ },
126
+ },
127
+ {
128
+ name: "answer_fields",
129
+ description: 'Answer structured requested facts by fact_id. Values must be strings; yes_no fields take "yes" or "no", and date fields use YYYY-MM-DD.',
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ fields: {
134
+ type: "object",
135
+ additionalProperties: { type: "string" },
136
+ description: "String values keyed by fact_id.",
137
+ },
138
+ session_id: { type: "string" },
139
+ task_id: { type: "string" },
140
+ },
141
+ required: ["fields"],
142
+ },
143
+ },
144
+ {
145
+ name: "upload_document",
146
+ description: "Upload a requested applicant document. Provide exactly one of file_path or content_base64. Inline content_base64 is limited to 5MB decoded; use file_path for larger files up to 25MB.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ file_path: { type: "string" },
151
+ content_base64: { type: "string" },
152
+ filename: {
153
+ type: "string",
154
+ description: "Required with content_base64. Defaults to the basename of file_path.",
155
+ },
156
+ mime_type: { type: "string" },
157
+ session_id: { type: "string" },
158
+ task_id: { type: "string" },
159
+ },
160
+ },
161
+ },
162
+ {
163
+ name: "submit_intake",
164
+ description: "Submit the applicant intake. Returns submitted, blockers, and the fresh intake state.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ session_id: { type: "string" },
169
+ field_values: {
170
+ type: "object",
171
+ additionalProperties: true,
172
+ description: "Optional final field values; each value must be string or null.",
173
+ },
174
+ },
175
+ },
176
+ },
177
+ {
178
+ name: "save_intake_draft",
179
+ description: "Save applicant form draft fields without submitting. Field values must be strings or null.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ fields: {
184
+ type: "object",
185
+ additionalProperties: true,
186
+ description: "Draft field values keyed by field_id.",
187
+ },
188
+ session_id: { type: "string" },
189
+ },
190
+ required: ["fields"],
191
+ },
192
+ },
193
+ {
194
+ name: "wait_for_next_action",
195
+ description: "Poll until the intake leaves processing or the timeout expires. If timed_out is true, call wait_for_next_action again.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ session_id: { type: "string" },
200
+ timeout_seconds: {
201
+ type: "number",
202
+ default: 60,
203
+ maximum: 120,
204
+ },
205
+ },
206
+ },
207
+ },
208
+ ];
209
+ const defaultIntakeRegistry = new IntakeSessionRegistry();
210
+ function createClient() {
211
+ const apiKey = process.env.KLAREFI_API_KEY?.trim();
212
+ if (!apiKey) {
213
+ throw new Error("KLAREFI_API_KEY is required.");
214
+ }
215
+ const baseUrl = process.env.KLAREFI_API_BASE_URL?.trim();
216
+ return new Klarefi({ apiKey, ...(baseUrl ? { baseUrl } : {}) });
217
+ }
218
+ export function requiredString(args, key) {
219
+ const value = args[key];
220
+ if (typeof value !== "string" || !value.trim()) {
221
+ throw new Error(`${key} is required.`);
222
+ }
223
+ return value.trim();
224
+ }
225
+ function optionalString(args, key) {
226
+ const value = args[key];
227
+ if (value === undefined || value === null) {
228
+ return undefined;
229
+ }
230
+ if (typeof value !== "string") {
231
+ throw new Error(`${key} must be a string.`);
232
+ }
233
+ const trimmed = value.trim();
234
+ return trimmed || undefined;
235
+ }
236
+ function isRecord(value) {
237
+ return value !== null && typeof value === "object" && !Array.isArray(value);
238
+ }
239
+ function requiredRecord(args, key) {
240
+ const value = args[key];
241
+ if (!isRecord(value)) {
242
+ throw new Error(`${key} is required and must be an object.`);
243
+ }
244
+ return value;
245
+ }
246
+ function requiredStringRecord(args, key) {
247
+ const record = requiredRecord(args, key);
248
+ const fields = {};
249
+ for (const [fieldKey, value] of Object.entries(record)) {
250
+ if (typeof value !== "string") {
251
+ throw new Error(`${key}.${fieldKey} must be a string.`);
252
+ }
253
+ fields[fieldKey] = value;
254
+ }
255
+ return fields;
256
+ }
257
+ function nullableStringRecord(value, key) {
258
+ if (!isRecord(value)) {
259
+ throw new Error(`${key} must be an object.`);
260
+ }
261
+ const fields = {};
262
+ for (const [fieldKey, fieldValue] of Object.entries(value)) {
263
+ if (fieldValue !== null && typeof fieldValue !== "string") {
264
+ throw new Error(`${key}.${fieldKey} must be a string or null.`);
265
+ }
266
+ fields[fieldKey] = fieldValue;
267
+ }
268
+ return fields;
269
+ }
270
+ function optionalNullableStringRecord(args, key) {
271
+ const value = args[key];
272
+ if (value === undefined || value === null) {
273
+ return undefined;
274
+ }
275
+ return nullableStringRecord(value, key);
276
+ }
277
+ function textResult(value) {
278
+ return {
279
+ content: [
280
+ {
281
+ type: "text",
282
+ text: JSON.stringify(value, null, 2),
283
+ },
284
+ ],
285
+ };
286
+ }
287
+ function decodedBase64Size(value) {
288
+ const normalized = value.replace(/\s+/g, "");
289
+ const padding = normalized.endsWith("==")
290
+ ? 2
291
+ : normalized.endsWith("=")
292
+ ? 1
293
+ : 0;
294
+ return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
295
+ }
296
+ function validateOpenIntakeArgs(args) {
297
+ const url = optionalString(args, "url");
298
+ const sessionId = optionalString(args, "session_id");
299
+ const accessToken = optionalString(args, "access_token");
300
+ if (url && (sessionId || accessToken)) {
301
+ throw new Error("Provide either url or session_id with access_token, not both.");
302
+ }
303
+ if (!url && (!sessionId || !accessToken)) {
304
+ throw new Error("open_intake requires either url or both session_id and access_token.");
305
+ }
306
+ return {
307
+ url,
308
+ session_id: sessionId,
309
+ access_token: accessToken,
310
+ api_base_url: optionalString(args, "api_base_url"),
311
+ };
312
+ }
313
+ async function uploadDocumentArgs(args) {
314
+ const filePath = optionalString(args, "file_path");
315
+ const contentBase64 = optionalString(args, "content_base64");
316
+ if (Boolean(filePath) === Boolean(contentBase64)) {
317
+ throw new Error("Provide exactly one of file_path or content_base64 for upload_document.");
318
+ }
319
+ const filename = optionalString(args, "filename");
320
+ const mimeType = optionalString(args, "mime_type");
321
+ const taskId = optionalString(args, "task_id");
322
+ if (contentBase64) {
323
+ if (!filename) {
324
+ throw new Error("filename is required when content_base64 is provided.");
325
+ }
326
+ const expectedBytes = decodedBase64Size(contentBase64);
327
+ if (expectedBytes > MAX_INLINE_UPLOAD_BYTES) {
328
+ throw new Error("content_base64 uploads are limited to 5MB decoded. Use file_path for larger documents up to 25MB.");
329
+ }
330
+ const bytes = Buffer.from(contentBase64, "base64");
331
+ if (bytes.byteLength > MAX_INLINE_UPLOAD_BYTES) {
332
+ throw new Error("content_base64 uploads are limited to 5MB decoded. Use file_path for larger documents up to 25MB.");
333
+ }
334
+ return {
335
+ file: bytes,
336
+ filename,
337
+ ...(mimeType ? { mimeType } : {}),
338
+ ...(taskId ? { taskId } : {}),
339
+ };
340
+ }
341
+ const requiredFilePath = filePath ?? "";
342
+ const file = await stat(requiredFilePath);
343
+ if (file.size > MAX_UPLOAD_BYTES) {
344
+ throw new Error("Document uploads are limited to 25MB.");
345
+ }
346
+ return {
347
+ file: requiredFilePath,
348
+ filename: filename ?? basename(requiredFilePath),
349
+ ...(mimeType ? { mimeType } : {}),
350
+ ...(taskId ? { taskId } : {}),
351
+ };
352
+ }
353
+ async function callToolValue(name, args, deps) {
354
+ if (name === "create_connector_from_openapi") {
355
+ return await deps
356
+ .createClient()
357
+ .connectors.importOpenApi(args);
358
+ }
359
+ if (name === "create_connector") {
360
+ return await deps
361
+ .createClient()
362
+ .connectors.create(args);
363
+ }
364
+ if (name === "delete_connector") {
365
+ return await deps
366
+ .createClient()
367
+ .connectors.delete(requiredString(args, "connector_key"));
368
+ }
369
+ if (name === "create_intake_session") {
370
+ return await deps
371
+ .createClient()
372
+ .sessions.create(args);
373
+ }
374
+ if (name === "get_case") {
375
+ return await deps
376
+ .createClient()
377
+ .cases.retrieve(requiredString(args, "case_id"));
378
+ }
379
+ if (name === "open_intake") {
380
+ const session = deps.intakeRegistry.open(validateOpenIntakeArgs(args));
381
+ return await session.getState();
382
+ }
383
+ if (name === "get_intake_state") {
384
+ return await deps.intakeRegistry
385
+ .get(optionalString(args, "session_id"))
386
+ .getState();
387
+ }
388
+ if (name === "answer_question") {
389
+ const taskId = optionalString(args, "task_id");
390
+ return await deps.intakeRegistry
391
+ .get(optionalString(args, "session_id"))
392
+ .answer(requiredString(args, "text"), {
393
+ ...(taskId ? { taskId } : {}),
394
+ });
395
+ }
396
+ if (name === "answer_fields") {
397
+ const taskId = optionalString(args, "task_id");
398
+ return await deps.intakeRegistry
399
+ .get(optionalString(args, "session_id"))
400
+ .answerFields(requiredStringRecord(args, "fields"), {
401
+ ...(taskId ? { taskId } : {}),
402
+ });
403
+ }
404
+ if (name === "upload_document") {
405
+ const params = await uploadDocumentArgs(args);
406
+ return await deps.intakeRegistry
407
+ .get(optionalString(args, "session_id"))
408
+ .uploadDocument(params);
409
+ }
410
+ if (name === "submit_intake") {
411
+ const fieldValues = optionalNullableStringRecord(args, "field_values");
412
+ return await deps.intakeRegistry
413
+ .get(optionalString(args, "session_id"))
414
+ .submit({
415
+ ...(fieldValues ? { fieldValues } : {}),
416
+ });
417
+ }
418
+ if (name === "save_intake_draft") {
419
+ return await deps.intakeRegistry
420
+ .get(optionalString(args, "session_id"))
421
+ .saveDraft(nullableStringRecord(args.fields, "fields"));
422
+ }
423
+ if (name === "wait_for_next_action") {
424
+ const timeoutSecondsValue = args.timeout_seconds ?? 60;
425
+ if (typeof timeoutSecondsValue !== "number" ||
426
+ !Number.isFinite(timeoutSecondsValue) ||
427
+ timeoutSecondsValue < 0 ||
428
+ timeoutSecondsValue > 120) {
429
+ throw new Error("timeout_seconds must be a number between 0 and 120.");
430
+ }
431
+ return await deps.intakeRegistry
432
+ .get(optionalString(args, "session_id"))
433
+ .nextAction({ timeoutMs: timeoutSecondsValue * 1000 });
434
+ }
435
+ throw new Error(`Unknown tool: ${name}`);
436
+ }
437
+ export function createToolDispatcher(deps = {}) {
438
+ const resolvedDeps = {
439
+ intakeRegistry: deps.intakeRegistry ?? defaultIntakeRegistry,
440
+ createClient: deps.createClient ?? createClient,
441
+ };
442
+ return {
443
+ tools,
444
+ callTool: async (name, args = {}) => textResult(await callToolValue(name, args, resolvedDeps)),
445
+ };
446
+ }
447
+ const defaultDispatcher = createToolDispatcher();
448
+ export async function callTool(name, args = {}) {
449
+ return defaultDispatcher.callTool(name, args);
450
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@klarefi/mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for Klarefi connector installation, intake sessions, and case retrieval.",
5
+ "type": "module",
6
+ "license": "ISC",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Jmqcooper1/Klarefi.git",
10
+ "directory": "packages/klarefi-mcp"
11
+ },
12
+ "bin": {
13
+ "klarefi-mcp": "./dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "prepack": "bun run build",
28
+ "test": "node --test test/*.test.mjs",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "@klarefi/node": "^0.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "typescript": "^5.0.0"
37
+ }
38
+ }