@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/README.md +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +156 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/sessions.js +92 -0
- package/dist/tools.d.ts +570 -0
- package/dist/tools.js +450 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @klarefi/mcp
|
|
2
|
+
|
|
3
|
+
MCP stdio server for agent access to Klarefi. It wraps the
|
|
4
|
+
[`@klarefi/node`](../klarefi-node) SDK for customer-plane API calls and the
|
|
5
|
+
applicant-plane intake driver.
|
|
6
|
+
|
|
7
|
+
Applicant-plane tools work from a signed intake URL or from
|
|
8
|
+
`session_id`/`access_token`; they do not require `KLAREFI_API_KEY`.
|
|
9
|
+
Customer-plane tools require `KLAREFI_API_KEY`.
|
|
10
|
+
|
|
11
|
+
## Client config
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"klarefi": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@klarefi/mcp"],
|
|
19
|
+
"env": {
|
|
20
|
+
"KLAREFI_API_KEY": "sk_test_...",
|
|
21
|
+
"KLAREFI_API_BASE_URL": "https://api.klarefi.com"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Omit `KLAREFI_API_KEY` when you only need applicant-plane intake tools.
|
|
29
|
+
`KLAREFI_API_BASE_URL` is optional and defaults to `https://api.klarefi.com`.
|
|
30
|
+
|
|
31
|
+
## Applicant-plane tools
|
|
32
|
+
|
|
33
|
+
- `open_intake` - open an intake from `url` or from `session_id` plus
|
|
34
|
+
`access_token`; returns the current intake state.
|
|
35
|
+
- `get_intake_state` - return the selected or most recently opened intake
|
|
36
|
+
state.
|
|
37
|
+
- `answer_question` - answer an active applicant question with free text.
|
|
38
|
+
- `answer_fields` - answer structured requested facts by `fact_id`; `yes_no`
|
|
39
|
+
values are `"yes"` or `"no"`, and dates use `YYYY-MM-DD`.
|
|
40
|
+
- `upload_document` - upload by `file_path` or inline `content_base64`.
|
|
41
|
+
Inline uploads are limited to 5MB decoded; use `file_path` for documents up
|
|
42
|
+
to 25MB.
|
|
43
|
+
- `submit_intake` - submit the intake and return `{ submitted, blockers,
|
|
44
|
+
state }`.
|
|
45
|
+
- `save_intake_draft` - save draft form fields without submitting.
|
|
46
|
+
- `wait_for_next_action` - poll for the next applicant action. If `timed_out`
|
|
47
|
+
is true, call it again.
|
|
48
|
+
|
|
49
|
+
## Customer-plane tools
|
|
50
|
+
|
|
51
|
+
- `create_connector_from_openapi` - import a public OpenAPI JSON URL as a
|
|
52
|
+
verification connector.
|
|
53
|
+
- `create_connector`
|
|
54
|
+
- `delete_connector`
|
|
55
|
+
- `create_intake_session`
|
|
56
|
+
- `get_case`
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { IntakeSessionRegistry } from "./sessions.js";
|
|
4
|
+
import { createToolDispatcher, requiredString, } from "./tools.js";
|
|
5
|
+
function readPackageVersion() {
|
|
6
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
7
|
+
if (typeof packageJson.version !== "string" || !packageJson.version) {
|
|
8
|
+
throw new Error("Package version is missing.");
|
|
9
|
+
}
|
|
10
|
+
return packageJson.version;
|
|
11
|
+
}
|
|
12
|
+
const SERVER_VERSION = readPackageVersion();
|
|
13
|
+
const intakeRegistry = new IntakeSessionRegistry();
|
|
14
|
+
const dispatcher = createToolDispatcher({ intakeRegistry });
|
|
15
|
+
function asJsonObject(value) {
|
|
16
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: {};
|
|
19
|
+
}
|
|
20
|
+
function writeMessage(message) {
|
|
21
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
22
|
+
}
|
|
23
|
+
function result(id, value) {
|
|
24
|
+
writeMessage({ jsonrpc: "2.0", id, result: value });
|
|
25
|
+
}
|
|
26
|
+
function errorResult(id, error) {
|
|
27
|
+
writeMessage({
|
|
28
|
+
jsonrpc: "2.0",
|
|
29
|
+
id,
|
|
30
|
+
error: {
|
|
31
|
+
code: -32000,
|
|
32
|
+
message: error instanceof Error ? error.message : String(error),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function handleRequest(request) {
|
|
37
|
+
if (request.id === undefined || request.id === null) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
if (request.method === "initialize") {
|
|
42
|
+
result(request.id, {
|
|
43
|
+
protocolVersion: "2024-11-05",
|
|
44
|
+
capabilities: { tools: {} },
|
|
45
|
+
serverInfo: { name: "klarefi-mcp", version: SERVER_VERSION },
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (request.method === "tools/list") {
|
|
50
|
+
result(request.id, { tools: [...dispatcher.tools] });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (request.method === "tools/call") {
|
|
54
|
+
const params = request.params ?? {};
|
|
55
|
+
const name = requiredString(params, "name");
|
|
56
|
+
const args = asJsonObject(params.arguments);
|
|
57
|
+
result(request.id, await dispatcher.callTool(name, args));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Unsupported MCP method: ${request.method ?? "(missing)"}`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
errorResult(request.id, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let buffer = Buffer.alloc(0);
|
|
67
|
+
let draining = false;
|
|
68
|
+
let drainAgain = false;
|
|
69
|
+
process.stdin.on("data", (chunk) => {
|
|
70
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
71
|
+
void drainMessages();
|
|
72
|
+
});
|
|
73
|
+
function headerBoundary(source) {
|
|
74
|
+
const crlf = source.indexOf("\r\n\r\n");
|
|
75
|
+
const lf = source.indexOf("\n\n");
|
|
76
|
+
if (crlf === -1 && lf === -1) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (crlf !== -1 && (lf === -1 || crlf < lf)) {
|
|
80
|
+
return { index: crlf, length: 4 };
|
|
81
|
+
}
|
|
82
|
+
return { index: lf, length: 2 };
|
|
83
|
+
}
|
|
84
|
+
function looksLikeContentLengthFrame(source) {
|
|
85
|
+
const prefix = source
|
|
86
|
+
.slice(0, Math.min(source.length, "Content-Length:".length))
|
|
87
|
+
.toString("utf8")
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
return "content-length:".startsWith(prefix) || prefix === "content-length:";
|
|
90
|
+
}
|
|
91
|
+
function readContentLengthFrame() {
|
|
92
|
+
if (!looksLikeContentLengthFrame(buffer)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const boundary = headerBoundary(buffer);
|
|
96
|
+
if (!boundary) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const header = buffer.slice(0, boundary.index).toString("utf8");
|
|
100
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
101
|
+
if (!match) {
|
|
102
|
+
throw new Error("Malformed Content-Length frame.");
|
|
103
|
+
}
|
|
104
|
+
const length = Number(match[1]);
|
|
105
|
+
const messageStart = boundary.index + boundary.length;
|
|
106
|
+
const messageEnd = messageStart + length;
|
|
107
|
+
if (buffer.length < messageEnd) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const raw = buffer.slice(messageStart, messageEnd).toString("utf8");
|
|
111
|
+
buffer = buffer.slice(messageEnd);
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
114
|
+
function readNewlineFrame() {
|
|
115
|
+
const newline = buffer.indexOf("\n");
|
|
116
|
+
if (newline === -1) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const raw = buffer.slice(0, newline).toString("utf8").trim();
|
|
120
|
+
buffer = buffer.slice(newline + 1);
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
async function drainMessages() {
|
|
124
|
+
if (draining) {
|
|
125
|
+
drainAgain = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
draining = true;
|
|
129
|
+
try {
|
|
130
|
+
while (true) {
|
|
131
|
+
drainAgain = false;
|
|
132
|
+
const contentLengthFrame = readContentLengthFrame();
|
|
133
|
+
if (contentLengthFrame === null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (contentLengthFrame !== undefined) {
|
|
137
|
+
await handleRequest(JSON.parse(contentLengthFrame));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const newlineFrame = readNewlineFrame();
|
|
141
|
+
if (newlineFrame === null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!newlineFrame) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
await handleRequest(JSON.parse(newlineFrame));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
draining = false;
|
|
152
|
+
if (drainAgain) {
|
|
153
|
+
void drainMessages();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AgentIntakeState, IntakeSubmitResult, UploadDocumentParams } from "@klarefi/node/intake";
|
|
2
|
+
export interface IntakeSessionLike {
|
|
3
|
+
getState(): Promise<AgentIntakeState>;
|
|
4
|
+
answer(text: string, opts?: {
|
|
5
|
+
taskId?: string;
|
|
6
|
+
}): Promise<AgentIntakeState>;
|
|
7
|
+
answerFields(fields: Record<string, string>, opts?: {
|
|
8
|
+
taskId?: string;
|
|
9
|
+
}): Promise<AgentIntakeState>;
|
|
10
|
+
saveDraft(fields: Record<string, string | null>): Promise<AgentIntakeState>;
|
|
11
|
+
submit(opts?: {
|
|
12
|
+
fieldValues?: Record<string, string | null>;
|
|
13
|
+
}): Promise<IntakeSubmitResult>;
|
|
14
|
+
uploadDocument(params: UploadDocumentParams): Promise<AgentIntakeState>;
|
|
15
|
+
nextAction(opts?: {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
pollIntervalMs?: number;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
state: AgentIntakeState;
|
|
20
|
+
timed_out: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface OpenIntakeOptions {
|
|
24
|
+
url?: string;
|
|
25
|
+
session_id?: string;
|
|
26
|
+
access_token?: string;
|
|
27
|
+
api_base_url?: string;
|
|
28
|
+
}
|
|
29
|
+
export type IntakeSessionFactoryInput = {
|
|
30
|
+
kind: "signed_url";
|
|
31
|
+
url: string;
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
clientName: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "credentials";
|
|
36
|
+
sessionId: string;
|
|
37
|
+
accessToken: string;
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
clientName: string;
|
|
40
|
+
};
|
|
41
|
+
export type IntakeSessionFactory = (input: IntakeSessionFactoryInput) => IntakeSessionLike;
|
|
42
|
+
export interface IntakeSessionRegistryOptions {
|
|
43
|
+
createSession?: IntakeSessionFactory;
|
|
44
|
+
}
|
|
45
|
+
export declare class IntakeSessionRegistry {
|
|
46
|
+
#private;
|
|
47
|
+
constructor(options?: IntakeSessionRegistryOptions);
|
|
48
|
+
open(opts: OpenIntakeOptions): IntakeSessionLike;
|
|
49
|
+
get(sessionId?: string): IntakeSessionLike;
|
|
50
|
+
}
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { IntakeSession } from "@klarefi/node/intake";
|
|
2
|
+
const DEFAULT_API_BASE_URL = "https://api.klarefi.com";
|
|
3
|
+
function envApiBaseUrl() {
|
|
4
|
+
const value = process.env.KLAREFI_API_BASE_URL?.trim();
|
|
5
|
+
return value || undefined;
|
|
6
|
+
}
|
|
7
|
+
function resolveApiBaseUrl(value) {
|
|
8
|
+
if (typeof value === "string" && value.trim()) {
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
return envApiBaseUrl() ?? DEFAULT_API_BASE_URL;
|
|
12
|
+
}
|
|
13
|
+
function parseSessionIdFromSignedUrl(input) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(input);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error("Signed intake URL is malformed.");
|
|
20
|
+
}
|
|
21
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
22
|
+
if (segments.length !== 2 || segments[0] !== "s" || !segments[1]) {
|
|
23
|
+
throw new Error("Signed intake URL must have the path /s/{session_id}.");
|
|
24
|
+
}
|
|
25
|
+
return decodeURIComponent(segments[1]);
|
|
26
|
+
}
|
|
27
|
+
function createDefaultSession(input) {
|
|
28
|
+
if (input.kind === "signed_url") {
|
|
29
|
+
return IntakeSession.fromSignedUrl(input.url, {
|
|
30
|
+
baseUrl: input.baseUrl,
|
|
31
|
+
clientName: input.clientName,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return new IntakeSession({
|
|
35
|
+
sessionId: input.sessionId,
|
|
36
|
+
accessToken: input.accessToken,
|
|
37
|
+
baseUrl: input.baseUrl,
|
|
38
|
+
clientName: input.clientName,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export class IntakeSessionRegistry {
|
|
42
|
+
#sessions = new Map();
|
|
43
|
+
#createSession;
|
|
44
|
+
#latestSessionId = null;
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.#createSession = options.createSession ?? createDefaultSession;
|
|
47
|
+
}
|
|
48
|
+
open(opts) {
|
|
49
|
+
const apiBaseUrl = resolveApiBaseUrl(opts.api_base_url);
|
|
50
|
+
const clientName = "klarefi-mcp";
|
|
51
|
+
let sessionId;
|
|
52
|
+
let session;
|
|
53
|
+
if (opts.url) {
|
|
54
|
+
sessionId = parseSessionIdFromSignedUrl(opts.url);
|
|
55
|
+
session = this.#createSession({
|
|
56
|
+
kind: "signed_url",
|
|
57
|
+
url: opts.url,
|
|
58
|
+
baseUrl: apiBaseUrl,
|
|
59
|
+
clientName,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const rawSessionId = opts.session_id?.trim();
|
|
64
|
+
const rawAccessToken = opts.access_token?.trim();
|
|
65
|
+
if (!rawSessionId || !rawAccessToken) {
|
|
66
|
+
throw new Error("open_intake requires either url or both session_id and access_token.");
|
|
67
|
+
}
|
|
68
|
+
sessionId = rawSessionId;
|
|
69
|
+
session = this.#createSession({
|
|
70
|
+
kind: "credentials",
|
|
71
|
+
sessionId,
|
|
72
|
+
accessToken: rawAccessToken,
|
|
73
|
+
baseUrl: apiBaseUrl,
|
|
74
|
+
clientName,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
this.#sessions.set(sessionId, session);
|
|
78
|
+
this.#latestSessionId = sessionId;
|
|
79
|
+
return session;
|
|
80
|
+
}
|
|
81
|
+
get(sessionId) {
|
|
82
|
+
const requestedSessionId = sessionId?.trim() || this.#latestSessionId;
|
|
83
|
+
if (!requestedSessionId) {
|
|
84
|
+
throw new Error("No open intake session. Call open_intake first.");
|
|
85
|
+
}
|
|
86
|
+
const session = this.#sessions.get(requestedSessionId);
|
|
87
|
+
if (!session) {
|
|
88
|
+
throw new Error(`No open intake session for session_id "${requestedSessionId}". Call open_intake first.`);
|
|
89
|
+
}
|
|
90
|
+
return session;
|
|
91
|
+
}
|
|
92
|
+
}
|