@webbula/mcp 1.0.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 +150 -0
- package/dist/api/enums.d.ts +80 -0
- package/dist/api/enums.js +230 -0
- package/dist/api/enums.js.map +1 -0
- package/dist/api/types.d.ts +163 -0
- package/dist/api/types.js +8 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/webbula-client.d.ts +116 -0
- package/dist/api/webbula-client.js +249 -0
- package/dist/api/webbula-client.js.map +1 -0
- package/dist/config.d.ts +70 -0
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +76 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/stdio.d.ts +12 -0
- package/dist/server/stdio.js +30 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/tools/download-results.d.ts +35 -0
- package/dist/tools/download-results.js +62 -0
- package/dist/tools/download-results.js.map +1 -0
- package/dist/tools/errors.d.ts +10 -0
- package/dist/tools/errors.js +16 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/file-status.d.ts +35 -0
- package/dist/tools/file-status.js +54 -0
- package/dist/tools/file-status.js.map +1 -0
- package/dist/tools/get-credits.d.ts +31 -0
- package/dist/tools/get-credits.js +47 -0
- package/dist/tools/get-credits.js.map +1 -0
- package/dist/tools/hygiene-check.d.ts +49 -0
- package/dist/tools/hygiene-check.js +84 -0
- package/dist/tools/hygiene-check.js.map +1 -0
- package/dist/tools/persona-append.d.ts +62 -0
- package/dist/tools/persona-append.js +123 -0
- package/dist/tools/persona-append.js.map +1 -0
- package/dist/tools/register.d.ts +15 -0
- package/dist/tools/register.js +23 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/upload-file.d.ts +58 -0
- package/dist/tools/upload-file.js +119 -0
- package/dist/tools/upload-file.js.map +1 -0
- package/dist/tools/validate-lead.d.ts +102 -0
- package/dist/tools/validate-lead.js +192 -0
- package/dist/tools/validate-lead.js.map +1 -0
- package/dist/tools/verify-email.d.ts +41 -0
- package/dist/tools/verify-email.js +74 -0
- package/dist/tools/verify-email.js.map +1 -0
- package/dist/trust.d.ts +28 -0
- package/dist/trust.js +23 -0
- package/dist/trust.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { WEBBULA_PERSONA_DEFAULT_SCOPE } from "../config.js";
|
|
3
|
+
import { toToolError } from "./errors.js";
|
|
4
|
+
/** The PII attributes that can serve as a match key (at least one required). */
|
|
5
|
+
export const APPEND_MATCH_KEYS = [
|
|
6
|
+
"first_name",
|
|
7
|
+
"last_name",
|
|
8
|
+
"email",
|
|
9
|
+
"address",
|
|
10
|
+
"city",
|
|
11
|
+
"state",
|
|
12
|
+
"zip5",
|
|
13
|
+
"phone",
|
|
14
|
+
];
|
|
15
|
+
/** Zod raw shape shared by all four append tools (validated before any call). */
|
|
16
|
+
export const appendInputShape = {
|
|
17
|
+
first_name: z.string().optional().describe("Contact's given name."),
|
|
18
|
+
last_name: z.string().optional().describe("Contact's family name."),
|
|
19
|
+
email: z.string().email().optional().describe("Known email address, if any."),
|
|
20
|
+
address: z.string().optional().describe("Street address line."),
|
|
21
|
+
city: z.string().optional().describe("City."),
|
|
22
|
+
state: z.string().optional().describe("State (2-letter or full)."),
|
|
23
|
+
zip5: z.string().optional().describe("5-digit ZIP code."),
|
|
24
|
+
phone: z.string().optional().describe("Known phone number, if any."),
|
|
25
|
+
scope: z
|
|
26
|
+
.enum(["individual", "household", "both"])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Identity-resolution scope: 'individual', 'household', or 'both'. " +
|
|
29
|
+
"Defaults to the server's configured scope ('both')."),
|
|
30
|
+
};
|
|
31
|
+
/** Reshape a persona response into the agent-facing result (no fabrication). */
|
|
32
|
+
export function toAppendResult(filter, scope, response) {
|
|
33
|
+
const data = response.data;
|
|
34
|
+
const matched = response.status === "success" &&
|
|
35
|
+
data !== undefined &&
|
|
36
|
+
Object.keys(data).length > 0;
|
|
37
|
+
const result = { filter, scope, matched };
|
|
38
|
+
if (matched && data !== undefined)
|
|
39
|
+
result.data = data;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/** The four append tools (CSV rows 22–25). */
|
|
43
|
+
export const APPEND_TOOLS = [
|
|
44
|
+
{
|
|
45
|
+
name: "append_email",
|
|
46
|
+
title: "Append an email address",
|
|
47
|
+
filter: "Email",
|
|
48
|
+
description: "Append an email address to a contact using Webbula's identity resolution. " +
|
|
49
|
+
"Use when the record has a name and/or postal address but no email and you " +
|
|
50
|
+
"want to find one. Do not use if an email is already present — use verify_email " +
|
|
51
|
+
"instead. Provide at least one identifying attribute (name, address, or phone).",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "append_telephone",
|
|
55
|
+
title: "Append a phone number",
|
|
56
|
+
filter: "Telephone",
|
|
57
|
+
description: "Append a phone number (and line type) to a contact using identity resolution. " +
|
|
58
|
+
"Use when the record has a name and/or address but no phone number. Provide at " +
|
|
59
|
+
"least one identifying attribute (name, address, or email).",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "append_postal",
|
|
63
|
+
title: "Append a postal address",
|
|
64
|
+
filter: "Postal",
|
|
65
|
+
description: "Append a full postal address (address, city, state, ZIP) to a contact using " +
|
|
66
|
+
"identity resolution. Use when the record has a name and/or email but no mailing " +
|
|
67
|
+
"address. Provide at least one identifying attribute (name, email, or phone).",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "append_automotive",
|
|
71
|
+
title: "Append automotive data",
|
|
72
|
+
filter: "Automotive",
|
|
73
|
+
description: "Append automotive data (year, make, model) associated with a contact using " +
|
|
74
|
+
"identity resolution. Use when you want vehicle information for a known person. " +
|
|
75
|
+
"Provide at least one identifying attribute (name, address, email, or phone).",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** Split validated input into the persona PII `record` and the resolved scope. */
|
|
79
|
+
function toRequest(input) {
|
|
80
|
+
const { scope, ...record } = input;
|
|
81
|
+
const pii = {};
|
|
82
|
+
for (const key of APPEND_MATCH_KEYS) {
|
|
83
|
+
const value = record[key];
|
|
84
|
+
if (value !== undefined)
|
|
85
|
+
pii[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return { scope: scope ?? WEBBULA_PERSONA_DEFAULT_SCOPE, record: pii };
|
|
88
|
+
}
|
|
89
|
+
/** Register all four persona append tools against the MCP server. */
|
|
90
|
+
export function registerPersonaAppend(server, client) {
|
|
91
|
+
for (const spec of APPEND_TOOLS) {
|
|
92
|
+
server.registerTool(spec.name, {
|
|
93
|
+
title: spec.title,
|
|
94
|
+
description: spec.description,
|
|
95
|
+
inputSchema: appendInputShape,
|
|
96
|
+
}, async (input) => {
|
|
97
|
+
// At least one PII attribute must be present to resolve an identity
|
|
98
|
+
// (pre-flight, principle V — never call upstream with an empty key).
|
|
99
|
+
const hasKey = APPEND_MATCH_KEYS.some((key) => input[key] !== undefined);
|
|
100
|
+
if (!hasKey) {
|
|
101
|
+
return toToolError(new Error("At least one identifying attribute is required " +
|
|
102
|
+
"(name, email, postal address, or phone)."));
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const { scope, record } = toRequest(input);
|
|
106
|
+
const response = await client.personaAppend({
|
|
107
|
+
filter: spec.filter,
|
|
108
|
+
scope,
|
|
109
|
+
record,
|
|
110
|
+
});
|
|
111
|
+
const result = toAppendResult(spec.filter, scope, response);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
114
|
+
structuredContent: result,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return toToolError(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=persona-append.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persona-append.js","sourceRoot":"","sources":["../../src/tools/persona-append.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic tool registration (constitution principle IV).
|
|
3
|
+
*
|
|
4
|
+
* Both `server/stdio.ts` and `server/http.ts` call `registerTools()` with a
|
|
5
|
+
* freshly-built `McpServer` and a shared `WebbulaClient`, so the exposed tool set
|
|
6
|
+
* is identical across transports. Each tool lives in its own module and is wired
|
|
7
|
+
* in below.
|
|
8
|
+
*/
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import type { WebbulaClient } from "../api/webbula-client.js";
|
|
11
|
+
/**
|
|
12
|
+
* Register every Webbula MCP tool against `server`, using `client` as the single
|
|
13
|
+
* HTTP boundary to the Webbula API.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerTools(server: McpServer, client: WebbulaClient): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { registerDownloadResults } from "./download-results.js";
|
|
2
|
+
import { registerFileStatus } from "./file-status.js";
|
|
3
|
+
import { registerGetCredits } from "./get-credits.js";
|
|
4
|
+
import { registerHygieneCheck } from "./hygiene-check.js";
|
|
5
|
+
import { registerPersonaAppend } from "./persona-append.js";
|
|
6
|
+
import { registerUploadFile } from "./upload-file.js";
|
|
7
|
+
import { registerValidateLead } from "./validate-lead.js";
|
|
8
|
+
import { registerVerifyEmail } from "./verify-email.js";
|
|
9
|
+
/**
|
|
10
|
+
* Register every Webbula MCP tool against `server`, using `client` as the single
|
|
11
|
+
* HTTP boundary to the Webbula API.
|
|
12
|
+
*/
|
|
13
|
+
export function registerTools(server, client) {
|
|
14
|
+
registerVerifyEmail(server, client); // 001 US1
|
|
15
|
+
registerHygieneCheck(server, client); // 001 US2
|
|
16
|
+
registerGetCredits(server, client); // 001 US3
|
|
17
|
+
registerValidateLead(server, client); // 002 lead validation
|
|
18
|
+
registerUploadFile(server, client); // 003 US1 batch submit
|
|
19
|
+
registerFileStatus(server, client); // 003 US2 batch status
|
|
20
|
+
registerDownloadResults(server, client); // 003 US3 batch download
|
|
21
|
+
registerPersonaAppend(server, client); // 006 persona data append (4 tools)
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,MAAqB;IACpE,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;IAC/C,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;IAChD,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU;IAC9C,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,sBAAsB;IAC5D,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,uBAAuB;IAC3D,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,uBAAuB;IAC3D,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,yBAAyB;IAClE,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,oCAAoC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload_file` — submit a file for asynchronous batch hygiene (spec 003 US1).
|
|
3
|
+
* Maps to `POST /api/v5/task/run` and returns a `package_name` to track the task
|
|
4
|
+
* (contracts/upload_file.md).
|
|
5
|
+
*
|
|
6
|
+
* The file is supplied EITHER inline (base64 + filename, JSON `file_base64` mode)
|
|
7
|
+
* OR as a remote `data_source` (web/ftp/ssh) — never a local filesystem path, so
|
|
8
|
+
* both transports behave identically (FR-002, constitution IV). The extension and
|
|
9
|
+
* the inline-size limit are validated before any upstream call (FR-003).
|
|
10
|
+
*/
|
|
11
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import type { BatchRunResponse } from "../api/types.js";
|
|
14
|
+
import { type BatchRunParams, type WebbulaClient } from "../api/webbula-client.js";
|
|
15
|
+
export declare const UPLOAD_FILE_NAME = "upload_file";
|
|
16
|
+
export declare const UPLOAD_FILE_DESCRIPTION: string;
|
|
17
|
+
/** Accepted upload file extensions (FR-003). */
|
|
18
|
+
export declare const ALLOWED_UPLOAD_EXTENSIONS: readonly [".csv", ".txt", ".xlsx", ".xls"];
|
|
19
|
+
export declare const uploadFileInputShape: {
|
|
20
|
+
readonly profile: z.ZodOptional<z.ZodString>;
|
|
21
|
+
readonly content_base64: z.ZodOptional<z.ZodString>;
|
|
22
|
+
readonly filename: z.ZodOptional<z.ZodString>;
|
|
23
|
+
readonly data_source: z.ZodOptional<z.ZodEnum<["web", "ftp", "ssh"]>>;
|
|
24
|
+
readonly url: z.ZodOptional<z.ZodString>;
|
|
25
|
+
readonly host: z.ZodOptional<z.ZodString>;
|
|
26
|
+
readonly port: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
readonly username: z.ZodOptional<z.ZodString>;
|
|
28
|
+
readonly password: z.ZodOptional<z.ZodString>;
|
|
29
|
+
readonly path: z.ZodOptional<z.ZodString>;
|
|
30
|
+
readonly secure: z.ZodOptional<z.ZodBoolean>;
|
|
31
|
+
};
|
|
32
|
+
export type UploadFileInput = {
|
|
33
|
+
profile?: string;
|
|
34
|
+
content_base64?: string;
|
|
35
|
+
filename?: string;
|
|
36
|
+
data_source?: "web" | "ftp" | "ssh";
|
|
37
|
+
url?: string;
|
|
38
|
+
host?: string;
|
|
39
|
+
port?: number;
|
|
40
|
+
username?: string;
|
|
41
|
+
password?: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
secure?: boolean;
|
|
44
|
+
};
|
|
45
|
+
export interface BatchSubmissionResult {
|
|
46
|
+
package_name: string;
|
|
47
|
+
transaction?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate the one-of input rule + the extension/size limits, and build the
|
|
51
|
+
* client params. Throws a structured `validation` error before any upstream call
|
|
52
|
+
* (FR-001/FR-003). Exported for contract testing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildBatchRunParams(input: UploadFileInput): BatchRunParams;
|
|
55
|
+
/** Reshape the upstream `BatchRunResponse` into the agent-facing handle. */
|
|
56
|
+
export declare function toBatchSubmissionResult(response: BatchRunResponse): BatchSubmissionResult;
|
|
57
|
+
/** Register `upload_file` against the MCP server. */
|
|
58
|
+
export declare function registerUploadFile(server: McpServer, client: WebbulaClient): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { WebbulaApiError, } from "../api/webbula-client.js";
|
|
3
|
+
import { WEBBULA_BATCH_PROFILE, WEBBULA_MAX_INLINE_BYTES } from "../config.js";
|
|
4
|
+
import { toToolError } from "./errors.js";
|
|
5
|
+
export const UPLOAD_FILE_NAME = "upload_file";
|
|
6
|
+
export const UPLOAD_FILE_DESCRIPTION = "Submit a file of emails for asynchronous batch hygiene when the list is too " +
|
|
7
|
+
"large for the real-time tools. Provide the file inline (base64 + filename) or " +
|
|
8
|
+
"point at a remote source (web URL, FTP, or SSH). Returns a package_name to " +
|
|
9
|
+
"track with file_status and fetch with download_results. Accepts .csv, .txt, " +
|
|
10
|
+
".xlsx, .xls. Local filesystem paths are not supported.";
|
|
11
|
+
/** Accepted upload file extensions (FR-003). */
|
|
12
|
+
export const ALLOWED_UPLOAD_EXTENSIONS = [".csv", ".txt", ".xlsx", ".xls"];
|
|
13
|
+
export const uploadFileInputShape = {
|
|
14
|
+
profile: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Optional Webbula profile name. Defaults to the server's configured profile."),
|
|
18
|
+
content_base64: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Base64-encoded file content (≤ ~1 MiB). Use with `filename`."),
|
|
22
|
+
filename: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("File name (with a .csv/.txt/.xlsx/.xls extension). Required with `content_base64`."),
|
|
26
|
+
data_source: z
|
|
27
|
+
.enum(["web", "ftp", "ssh"])
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Remote source type, as an alternative to inline content."),
|
|
30
|
+
url: z.string().url().optional().describe("Source URL (when `data_source` is `web`)."),
|
|
31
|
+
host: z.string().optional().describe("FTP/SSH host."),
|
|
32
|
+
port: z.number().int().optional().describe("FTP/SSH port."),
|
|
33
|
+
username: z.string().optional().describe("FTP/SSH username."),
|
|
34
|
+
password: z.string().optional().describe("FTP/SSH password."),
|
|
35
|
+
path: z.string().optional().describe("FTP/SSH remote file path."),
|
|
36
|
+
secure: z.boolean().optional().describe("Use TLS (SFTP) for the FTP source."),
|
|
37
|
+
};
|
|
38
|
+
function hasAllowedExtension(filename) {
|
|
39
|
+
const lower = filename.toLowerCase();
|
|
40
|
+
return ALLOWED_UPLOAD_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate the one-of input rule + the extension/size limits, and build the
|
|
44
|
+
* client params. Throws a structured `validation` error before any upstream call
|
|
45
|
+
* (FR-001/FR-003). Exported for contract testing.
|
|
46
|
+
*/
|
|
47
|
+
export function buildBatchRunParams(input) {
|
|
48
|
+
const hasInline = input.content_base64 !== undefined || input.filename !== undefined;
|
|
49
|
+
const hasSource = input.data_source !== undefined;
|
|
50
|
+
if (hasInline && hasSource) {
|
|
51
|
+
throw new WebbulaApiError("validation", "Provide either content_base64+filename or a data_source, not both");
|
|
52
|
+
}
|
|
53
|
+
if (!hasInline && !hasSource) {
|
|
54
|
+
throw new WebbulaApiError("validation", "Provide either content_base64+filename or a data_source");
|
|
55
|
+
}
|
|
56
|
+
const profile = input.profile ?? WEBBULA_BATCH_PROFILE;
|
|
57
|
+
if (hasInline) {
|
|
58
|
+
if (input.content_base64 === undefined || input.filename === undefined) {
|
|
59
|
+
throw new WebbulaApiError("validation", "Inline upload requires both content_base64 and filename");
|
|
60
|
+
}
|
|
61
|
+
if (!hasAllowedExtension(input.filename)) {
|
|
62
|
+
throw new WebbulaApiError("validation", "Unsupported file type — use .csv, .txt, .xlsx, or .xls");
|
|
63
|
+
}
|
|
64
|
+
const decodedBytes = Buffer.from(input.content_base64, "base64").length;
|
|
65
|
+
if (decodedBytes > WEBBULA_MAX_INLINE_BYTES) {
|
|
66
|
+
throw new WebbulaApiError("validation", `Inline file exceeds the ~${Math.round(WEBBULA_MAX_INLINE_BYTES / 1_048_576)} MiB limit — use a data_source`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
profile,
|
|
70
|
+
inline: { filename: input.filename, content_base64: input.content_base64 },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Remote data source.
|
|
74
|
+
return {
|
|
75
|
+
profile,
|
|
76
|
+
dataSource: {
|
|
77
|
+
data_source: input.data_source,
|
|
78
|
+
url: input.url,
|
|
79
|
+
host: input.host,
|
|
80
|
+
port: input.port,
|
|
81
|
+
username: input.username,
|
|
82
|
+
password: input.password,
|
|
83
|
+
path: input.path,
|
|
84
|
+
secure: input.secure,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Reshape the upstream `BatchRunResponse` into the agent-facing handle. */
|
|
89
|
+
export function toBatchSubmissionResult(response) {
|
|
90
|
+
if (typeof response.package_name !== "string" || response.package_name === "") {
|
|
91
|
+
throw new WebbulaApiError("upstream", "Webbula accepted the task but returned no package_name");
|
|
92
|
+
}
|
|
93
|
+
const out = { package_name: response.package_name };
|
|
94
|
+
if (typeof response.transaction === "string")
|
|
95
|
+
out.transaction = response.transaction;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** Register `upload_file` against the MCP server. */
|
|
99
|
+
export function registerUploadFile(server, client) {
|
|
100
|
+
server.registerTool(UPLOAD_FILE_NAME, {
|
|
101
|
+
title: "Submit a batch file",
|
|
102
|
+
description: UPLOAD_FILE_DESCRIPTION,
|
|
103
|
+
inputSchema: uploadFileInputShape,
|
|
104
|
+
}, async (input) => {
|
|
105
|
+
try {
|
|
106
|
+
const params = buildBatchRunParams(input);
|
|
107
|
+
const response = await client.runBatch(params);
|
|
108
|
+
const result = toBatchSubmissionResult(response);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
111
|
+
structuredContent: result,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return toToolError(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=upload-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-file.js","sourceRoot":"","sources":["../../src/tools/upload-file.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EACL,eAAe,GAGhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC;AAE9C,MAAM,CAAC,MAAM,uBAAuB,GAClC,8EAA8E;IAC9E,gFAAgF;IAChF,6EAA6E;IAC7E,8EAA8E;IAC9E,wDAAwD,CAAC;AAE3D,gDAAgD;AAChD,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAU,CAAC;AAEpF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,6EAA6E,CAAC;IAC1F,cAAc,EAAE,CAAC;SACd,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,oFAAoF,CAAC;IACjG,WAAW,EAAE,CAAC;SACX,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;SAC3B,QAAQ,EAAE;SACV,QAAQ,CAAC,0DAA0D,CAAC;IACvE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;IACtF,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;IACrD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IAC7D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IAC7D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;IACjE,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;CACrE,CAAC;AAqBX,SAAS,mBAAmB,CAAC,QAAgB;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAsB;IACxD,MAAM,SAAS,GAAG,KAAK,CAAC,cAAc,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC;IACrF,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC;IAElD,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,eAAe,CACvB,YAAY,EACZ,mEAAmE,CACpE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,eAAe,CACvB,YAAY,EACZ,yDAAyD,CAC1D,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,qBAAqB,CAAC;IAEvD,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACvE,MAAM,IAAI,eAAe,CACvB,YAAY,EACZ,yDAAyD,CAC1D,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,eAAe,CACvB,YAAY,EACZ,wDAAwD,CACzD,CAAC;QACJ,CAAC;QACD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,MAAM,CAAC;QACxE,IAAI,YAAY,GAAG,wBAAwB,EAAE,CAAC;YAC5C,MAAM,IAAI,eAAe,CACvB,YAAY,EACZ,4BAA4B,IAAI,CAAC,KAAK,CACpC,wBAAwB,GAAG,SAAS,CACrC,gCAAgC,CAClC,CAAC;QACJ,CAAC;QACD,OAAO;YACL,OAAO;YACP,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE;SAC3E,CAAC;IACJ,CAAC;IAED,sBAAsB;IACtB,OAAO;QACL,OAAO;QACP,UAAU,EAAE;YACV,WAAW,EAAE,KAAK,CAAC,WAAY;YAC/B,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,uBAAuB,CACrC,QAA0B;IAE1B,IAAI,OAAO,QAAQ,CAAC,YAAY,KAAK,QAAQ,IAAI,QAAQ,CAAC,YAAY,KAAK,EAAE,EAAE,CAAC;QAC9E,MAAM,IAAI,eAAe,CACvB,UAAU,EACV,wDAAwD,CACzD,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAA0B,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC;IAC3E,IAAI,OAAO,QAAQ,CAAC,WAAW,KAAK,QAAQ;QAAE,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC;IACrF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,MAAqB;IACzE,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,uBAAuB;QACpC,WAAW,EAAE,oBAAoB;KAClC,EACD,KAAK,EAAE,KAAK,EAAE,EAAE;QACd,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAC1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;YACjD,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;gBAClE,iBAAiB,EAAE,MAA4C;aAChE,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validate_lead` — assess a full lead/contact record (spec 002 US1/US2). Maps to
|
|
3
|
+
* `POST /api/v5/task/lead` and reshapes the `ExpressCompoundResponse` into one
|
|
4
|
+
* agent-facing `LeadValidationResult` (contracts/validate_lead.md).
|
|
5
|
+
*
|
|
6
|
+
* Each supplied attribute is returned as its own sub-result; any sub-result the
|
|
7
|
+
* upstream omits is absent, never fabricated (FR-003). Documented coded fields
|
|
8
|
+
* (`name.gender`, name/phone `status`, `phone.service_type`) carry the verbatim
|
|
9
|
+
* upstream code AND a human-readable label/description from the STATIC tables in
|
|
10
|
+
* `api/enums.ts` — unknown codes omit the friendly field (FR-004, principle I).
|
|
11
|
+
* `address.accuracy_score` is passed through verbatim (FR-006). When `emails` are
|
|
12
|
+
* supplied, each reuses `verify_email`'s verdict + trust shape exactly (FR-005).
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import type { ExpressCompoundResponse } from "../api/types.js";
|
|
17
|
+
import { type WebbulaClient } from "../api/webbula-client.js";
|
|
18
|
+
import { type EmailVerificationResult } from "./verify-email.js";
|
|
19
|
+
export declare const VALIDATE_LEAD_NAME = "validate_lead";
|
|
20
|
+
export declare const VALIDATE_LEAD_DESCRIPTION: string;
|
|
21
|
+
/** The lead attributes that satisfy the "at least one attribute" rule (FR-002). */
|
|
22
|
+
export declare const LEAD_ATTRIBUTE_KEYS: readonly ["first_name", "last_name", "full_address", "phone", "emails"];
|
|
23
|
+
/**
|
|
24
|
+
* Zod raw shape for the tool input (validated before any upstream call). The
|
|
25
|
+
* v4 `task/lead` endpoint accepts only firstname/lastname/address/phone/emails,
|
|
26
|
+
* so the complete street+city+state+ZIP goes in `full_address`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const validateLeadInputShape: {
|
|
29
|
+
readonly profile: z.ZodOptional<z.ZodString>;
|
|
30
|
+
readonly first_name: z.ZodOptional<z.ZodString>;
|
|
31
|
+
readonly last_name: z.ZodOptional<z.ZodString>;
|
|
32
|
+
readonly full_address: z.ZodOptional<z.ZodString>;
|
|
33
|
+
readonly phone: z.ZodOptional<z.ZodString>;
|
|
34
|
+
readonly emails: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
35
|
+
};
|
|
36
|
+
export type ValidateLeadInput = {
|
|
37
|
+
profile?: string;
|
|
38
|
+
first_name?: string;
|
|
39
|
+
last_name?: string;
|
|
40
|
+
full_address?: string;
|
|
41
|
+
phone?: string;
|
|
42
|
+
emails?: string[];
|
|
43
|
+
};
|
|
44
|
+
export interface NameAssessment {
|
|
45
|
+
prefix?: string;
|
|
46
|
+
first?: string;
|
|
47
|
+
middle?: string;
|
|
48
|
+
last?: string;
|
|
49
|
+
suffix?: string;
|
|
50
|
+
gender?: string;
|
|
51
|
+
gender_label?: string;
|
|
52
|
+
status?: string;
|
|
53
|
+
status_description?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface AddressAssessment {
|
|
56
|
+
address?: string;
|
|
57
|
+
address2?: string;
|
|
58
|
+
city?: string;
|
|
59
|
+
state?: string;
|
|
60
|
+
zip5?: string;
|
|
61
|
+
lat?: number;
|
|
62
|
+
lon?: number;
|
|
63
|
+
/** Continuous percent, verbatim (FR-006 — no bucketing/rounding). */
|
|
64
|
+
accuracy_score?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface PhoneAssessment {
|
|
67
|
+
number?: string;
|
|
68
|
+
area_code?: string;
|
|
69
|
+
prefix?: string;
|
|
70
|
+
suffix?: string;
|
|
71
|
+
service_type?: string;
|
|
72
|
+
service_type_description?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
status_description?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface CreditScenario {
|
|
77
|
+
scenario?: string;
|
|
78
|
+
records?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface LeadValidationResult {
|
|
81
|
+
name?: NameAssessment;
|
|
82
|
+
address?: AddressAssessment;
|
|
83
|
+
phone?: PhoneAssessment;
|
|
84
|
+
emails?: EmailVerificationResult[];
|
|
85
|
+
profile?: string;
|
|
86
|
+
transaction?: string;
|
|
87
|
+
credits_remaining?: CreditScenario[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Throw a structured `validation` error when no lead attribute is supplied
|
|
91
|
+
* (FR-002). Enforced before any upstream call. Exported for contract testing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function assertAtLeastOneAttribute(input: ValidateLeadInput): void;
|
|
94
|
+
/**
|
|
95
|
+
* Reshape the upstream `ExpressCompoundResponse` into a `LeadValidationResult`.
|
|
96
|
+
* Any sub-result the upstream omits is left absent (FR-003); the email sub-result
|
|
97
|
+
* reuses `verify_email`'s verdict + trust mapping exactly (FR-005). Exported for
|
|
98
|
+
* contract testing.
|
|
99
|
+
*/
|
|
100
|
+
export declare function toLeadValidationResult(response: ExpressCompoundResponse): LeadValidationResult;
|
|
101
|
+
/** Register `validate_lead` against the MCP server. */
|
|
102
|
+
export declare function registerValidateLead(server: McpServer, client: WebbulaClient): void;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { NAME_GENDER, NAME_STATUS, PHONE_SERVICE_TYPE, PHONE_STATUS, } from "../api/enums.js";
|
|
3
|
+
import { WebbulaApiError, } from "../api/webbula-client.js";
|
|
4
|
+
import { WEBBULA_LEAD_PROFILE, WEBBULA_MAX_EMAILS } from "../config.js";
|
|
5
|
+
import { toVerificationResult } from "./verify-email.js";
|
|
6
|
+
import { toToolError } from "./errors.js";
|
|
7
|
+
export const VALIDATE_LEAD_NAME = "validate_lead";
|
|
8
|
+
export const VALIDATE_LEAD_DESCRIPTION = "Validate a full lead/contact record with Webbula's leadHygiene. Parses and " +
|
|
9
|
+
"assesses any combination of name, postal address, and phone number — modeled " +
|
|
10
|
+
"gender, address accuracy and geocode, phone line type — and, when an email is " +
|
|
11
|
+
"supplied, runs the same email hygiene verdict as verify_email in the same call. " +
|
|
12
|
+
"Use this for cleaning CRM/lead records, not just an email address. At least one " +
|
|
13
|
+
"of first_name, last_name, full_address, phone, or emails is required. " +
|
|
14
|
+
"Real-time, single tool call.";
|
|
15
|
+
/** The lead attributes that satisfy the "at least one attribute" rule (FR-002). */
|
|
16
|
+
export const LEAD_ATTRIBUTE_KEYS = [
|
|
17
|
+
"first_name",
|
|
18
|
+
"last_name",
|
|
19
|
+
"full_address",
|
|
20
|
+
"phone",
|
|
21
|
+
"emails",
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Zod raw shape for the tool input (validated before any upstream call). The
|
|
25
|
+
* v4 `task/lead` endpoint accepts only firstname/lastname/address/phone/emails,
|
|
26
|
+
* so the complete street+city+state+ZIP goes in `full_address`.
|
|
27
|
+
*/
|
|
28
|
+
export const validateLeadInputShape = {
|
|
29
|
+
profile: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Optional lead-enabled Webbula profile name. Defaults to the server's configured lead profile."),
|
|
33
|
+
first_name: z.string().optional().describe("Given name."),
|
|
34
|
+
last_name: z.string().optional().describe("Family name."),
|
|
35
|
+
full_address: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("The complete address: street, city, state, and ZIP."),
|
|
39
|
+
phone: z.string().optional().describe("Phone number."),
|
|
40
|
+
emails: z
|
|
41
|
+
.array(z.string().email())
|
|
42
|
+
.max(WEBBULA_MAX_EMAILS)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(`Optional email addresses to additionally run hygiene on (max ${WEBBULA_MAX_EMAILS}).`),
|
|
45
|
+
};
|
|
46
|
+
/** Copy a defined string field from src→dst under the same key. */
|
|
47
|
+
function copyString(src, dst, key) {
|
|
48
|
+
const value = src[key];
|
|
49
|
+
if (typeof value === "string")
|
|
50
|
+
dst[key] = value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Throw a structured `validation` error when no lead attribute is supplied
|
|
54
|
+
* (FR-002). Enforced before any upstream call. Exported for contract testing.
|
|
55
|
+
*/
|
|
56
|
+
export function assertAtLeastOneAttribute(input) {
|
|
57
|
+
const hasAttribute = LEAD_ATTRIBUTE_KEYS.some((key) => {
|
|
58
|
+
const value = input[key];
|
|
59
|
+
return key === "emails"
|
|
60
|
+
? Array.isArray(value) && value.length > 0
|
|
61
|
+
: value !== undefined && value !== "";
|
|
62
|
+
});
|
|
63
|
+
if (!hasAttribute) {
|
|
64
|
+
throw new WebbulaApiError("validation", "Supply at least one of: first_name, last_name, full_address, phone, ip, emails");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Reshape the upstream `name` sub-result, enriching coded fields (FR-004). */
|
|
68
|
+
function toNameAssessment(name) {
|
|
69
|
+
const out = {};
|
|
70
|
+
const src = name;
|
|
71
|
+
const dst = out;
|
|
72
|
+
copyString(src, dst, "prefix");
|
|
73
|
+
copyString(src, dst, "first");
|
|
74
|
+
copyString(src, dst, "middle");
|
|
75
|
+
copyString(src, dst, "last");
|
|
76
|
+
copyString(src, dst, "suffix");
|
|
77
|
+
if (typeof name.gender === "string") {
|
|
78
|
+
out.gender = name.gender;
|
|
79
|
+
const info = Object.prototype.hasOwnProperty.call(NAME_GENDER, name.gender)
|
|
80
|
+
? NAME_GENDER[name.gender]
|
|
81
|
+
: undefined;
|
|
82
|
+
if (info !== undefined)
|
|
83
|
+
out.gender_label = info.label;
|
|
84
|
+
}
|
|
85
|
+
if (typeof name.status === "string") {
|
|
86
|
+
out.status = name.status;
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(NAME_STATUS, name.status)) {
|
|
88
|
+
out.status_description = NAME_STATUS[name.status];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** Reshape the upstream `address` sub-result (accuracy_score verbatim, FR-006). */
|
|
94
|
+
function toAddressAssessment(address) {
|
|
95
|
+
const out = {};
|
|
96
|
+
const src = address;
|
|
97
|
+
const dst = out;
|
|
98
|
+
copyString(src, dst, "address");
|
|
99
|
+
copyString(src, dst, "address2");
|
|
100
|
+
copyString(src, dst, "city");
|
|
101
|
+
copyString(src, dst, "state");
|
|
102
|
+
copyString(src, dst, "zip5");
|
|
103
|
+
copyString(src, dst, "accuracy_score");
|
|
104
|
+
if (typeof address.lat === "number")
|
|
105
|
+
out.lat = address.lat;
|
|
106
|
+
if (typeof address.lon === "number")
|
|
107
|
+
out.lon = address.lon;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** Reshape the upstream `phone` sub-result, enriching coded fields (FR-004). */
|
|
111
|
+
function toPhoneAssessment(phone) {
|
|
112
|
+
const out = {};
|
|
113
|
+
const src = phone;
|
|
114
|
+
const dst = out;
|
|
115
|
+
copyString(src, dst, "number");
|
|
116
|
+
copyString(src, dst, "area_code");
|
|
117
|
+
copyString(src, dst, "prefix");
|
|
118
|
+
copyString(src, dst, "suffix");
|
|
119
|
+
if (typeof phone.service_type === "string") {
|
|
120
|
+
out.service_type = phone.service_type;
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(PHONE_SERVICE_TYPE, phone.service_type)) {
|
|
122
|
+
out.service_type_description = PHONE_SERVICE_TYPE[phone.service_type];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (typeof phone.status === "string") {
|
|
126
|
+
out.status = phone.status;
|
|
127
|
+
if (Object.prototype.hasOwnProperty.call(PHONE_STATUS, phone.status)) {
|
|
128
|
+
out.status_description = PHONE_STATUS[phone.status];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Reshape the upstream `ExpressCompoundResponse` into a `LeadValidationResult`.
|
|
135
|
+
* Any sub-result the upstream omits is left absent (FR-003); the email sub-result
|
|
136
|
+
* reuses `verify_email`'s verdict + trust mapping exactly (FR-005). Exported for
|
|
137
|
+
* contract testing.
|
|
138
|
+
*/
|
|
139
|
+
export function toLeadValidationResult(response) {
|
|
140
|
+
const out = {};
|
|
141
|
+
if (response.name !== undefined)
|
|
142
|
+
out.name = toNameAssessment(response.name);
|
|
143
|
+
if (response.address !== undefined)
|
|
144
|
+
out.address = toAddressAssessment(response.address);
|
|
145
|
+
if (response.phone !== undefined)
|
|
146
|
+
out.phone = toPhoneAssessment(response.phone);
|
|
147
|
+
if (response.emails !== undefined && response.emails.length > 0) {
|
|
148
|
+
out.emails = response.emails.map(toVerificationResult);
|
|
149
|
+
}
|
|
150
|
+
if (typeof response.profile === "string")
|
|
151
|
+
out.profile = response.profile;
|
|
152
|
+
if (typeof response.transaction === "string")
|
|
153
|
+
out.transaction = response.transaction;
|
|
154
|
+
if (Array.isArray(response.credits_remaining)) {
|
|
155
|
+
out.credits_remaining = response.credits_remaining.map((entry) => {
|
|
156
|
+
const scenario = {};
|
|
157
|
+
if (typeof entry.scenario === "string")
|
|
158
|
+
scenario.scenario = entry.scenario;
|
|
159
|
+
if (typeof entry.records === "string")
|
|
160
|
+
scenario.records = entry.records;
|
|
161
|
+
return scenario;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/** Register `validate_lead` against the MCP server. */
|
|
167
|
+
export function registerValidateLead(server, client) {
|
|
168
|
+
server.registerTool(VALIDATE_LEAD_NAME, {
|
|
169
|
+
title: "Validate a lead record",
|
|
170
|
+
description: VALIDATE_LEAD_DESCRIPTION,
|
|
171
|
+
inputSchema: validateLeadInputShape,
|
|
172
|
+
}, async (input) => {
|
|
173
|
+
try {
|
|
174
|
+
assertAtLeastOneAttribute(input);
|
|
175
|
+
const { profile, ...attributes } = input;
|
|
176
|
+
const params = {
|
|
177
|
+
profile: profile ?? WEBBULA_LEAD_PROFILE,
|
|
178
|
+
...attributes,
|
|
179
|
+
};
|
|
180
|
+
const response = await client.leadValidate(params);
|
|
181
|
+
const result = toLeadValidationResult(response);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
184
|
+
structuredContent: result,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return toToolError(error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=validate-lead.js.map
|