uniqueness-mcp 0.4.2 → 0.5.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/index.mjs +327 -143
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -1,174 +1,358 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Uniqueness Engine MCP server.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// { "mcpServers": { "uniqueness": {
|
|
7
|
-
// "command": "npx", "args": ["-y", "uniqueness-mcp"],
|
|
8
|
-
// "env": { "UNIQUENESS_API_KEY": "uq_live_..." } } } }
|
|
9
|
-
|
|
2
|
+
// Uniqueness Engine MCP server. This is a thin, conservative wrapper around
|
|
3
|
+
// the public API: it can ask a human to act, but it never purchases a pack,
|
|
4
|
+
// saves a payment method, enables auto-recharge, or subscribes by inference.
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
10
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
8
|
import { z } from "zod";
|
|
13
9
|
|
|
14
10
|
const BASE = process.env.UNIQUENESS_API_URL || "https://uniquenessengine.com";
|
|
15
|
-
|
|
11
|
+
let KEY = process.env.UNIQUENESS_API_KEY || "";
|
|
12
|
+
const signupContinuations = new Map();
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
const server = new McpServer({ name: "uniqueness", version: "0.4.2" });
|
|
14
|
+
const server = new McpServer({ name: "uniqueness", version: "0.5.0" });
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
16
|
+
const json = (value, isError = false) => ({
|
|
17
|
+
...(isError ? { isError: true } : {}),
|
|
18
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function pruneSignupContinuations() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
for (const [id, continuation] of signupContinuations) {
|
|
24
|
+
if (continuation.expiresAt <= now) signupContinuations.delete(id);
|
|
25
|
+
}
|
|
26
|
+
while (signupContinuations.size > 100)
|
|
27
|
+
signupContinuations.delete(signupContinuations.keys().next().value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function keyHeaders(body, auth) {
|
|
31
|
+
const headers = {};
|
|
32
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
33
|
+
if (auth && KEY) headers.authorization = `Bearer ${KEY}`;
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function api(path, { method = "POST", body, auth = true } = {}) {
|
|
38
|
+
if (auth && !KEY) return { status: 401, body: { error: "missing_api_key" } };
|
|
39
|
+
const response = await fetch(`${BASE}${path}`, {
|
|
40
|
+
method,
|
|
41
|
+
headers: keyHeaders(body, auth),
|
|
42
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
43
|
+
});
|
|
44
|
+
return { status: response.status, body: await response.json().catch(() => ({})) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPaymentRequired(value) {
|
|
48
|
+
return value && value.error === "payment_required" && value.reason === "insufficient_credits" &&
|
|
49
|
+
value.credits && value.retry && typeof value.billing_url === "string";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function spendableBalance(value) {
|
|
53
|
+
const candidates = [
|
|
54
|
+
value?.total_credits,
|
|
55
|
+
value?.credits_remaining,
|
|
56
|
+
value?.balance?.total_credits,
|
|
57
|
+
value?.balance?.spendable_credits,
|
|
58
|
+
];
|
|
59
|
+
return candidates.find((candidate) => typeof candidate === "number" && Number.isFinite(candidate)) ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function paymentAction(contract) {
|
|
63
|
+
return json({
|
|
64
|
+
status: "human_action_required",
|
|
65
|
+
action: "checkout",
|
|
66
|
+
message: "Ask a human before surfacing a purchase action. After explicit consent, use payment_required.recommended_offer.checkout_url for the offered pack, or payment_required.billing_url if the human wants to choose another pack. This MCP server has not bought anything, saved a payment method, enabled auto-recharge, or started a subscription. It stores no unpaid target. After payment, the host may call resume_connection_brief with the original target, the returned retry.idempotency_key, and the previously observed available balance; the service verifies both the new balance and retry identity.",
|
|
67
|
+
payment_required: contract,
|
|
68
|
+
resume_tool: "resume_connection_brief",
|
|
69
|
+
resume_requires: [
|
|
70
|
+
"original target fields",
|
|
71
|
+
"payment_required.retry.idempotency_key",
|
|
72
|
+
"payment_required.credits.available",
|
|
73
|
+
],
|
|
74
|
+
fixture: contract.fixture,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function authenticationRequired() {
|
|
79
|
+
return json({
|
|
80
|
+
status: "human_action_required",
|
|
81
|
+
action: "programmatic_signup",
|
|
82
|
+
message: "No API key is configured. Begin named email approval; do not use a key from another person or assume approval.",
|
|
83
|
+
begin_tool: "begin_programmatic_signup",
|
|
84
|
+
}, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function pollBrief(jobId) {
|
|
88
|
+
const deadline = Date.now() + 210000;
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
91
|
+
const poll = await api(`/api/jobs/${jobId}`, { method: "GET" });
|
|
92
|
+
if (poll.body.status === "done") return { kind: "brief", brief: poll.body.result };
|
|
93
|
+
if (poll.body.status === "refused") return { kind: "brief", brief: poll.body.result || { status: "needs_review", note: "Identity could not be confidently resolved." } };
|
|
94
|
+
if (poll.body.status === "failed") return { kind: "error", error: poll.body.error || "job_failed" };
|
|
95
|
+
}
|
|
96
|
+
return { kind: "pending", job_id: jobId };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function submitBrief(args, idempotencyKey) {
|
|
100
|
+
const payload = { ...args, idempotency_key: idempotencyKey };
|
|
101
|
+
const submit = await api("/api/enrich", { body: payload });
|
|
102
|
+
if (submit.status === 402 && isPaymentRequired(submit.body)) return { kind: "payment_required", contract: submit.body };
|
|
103
|
+
if (submit.status === 401) return { kind: "unauthorized" };
|
|
104
|
+
if (submit.status === 409 && (submit.body.error === "idempotency_key_expired" || submit.body.error === "idempotency_key_mismatch"))
|
|
105
|
+
return { kind: "retry_rejected", error: submit.body.error };
|
|
106
|
+
if (submit.status >= 400) return { kind: "error", error: submit.body.error || `http_${submit.status}`, message: submit.body.message };
|
|
107
|
+
if (submit.body.brief) return { kind: "brief", brief: submit.body.brief };
|
|
108
|
+
if (!submit.body.job_id) return { kind: "error", error: "invalid_enrich_response" };
|
|
109
|
+
return pollBrief(submit.body.job_id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function briefInputSchema() {
|
|
113
|
+
return {
|
|
36
114
|
linkedin_url: z.string().optional().describe("LinkedIn profile URL"),
|
|
37
115
|
name: z.string().optional().describe("Full name (use with company)"),
|
|
38
116
|
company: z.string().optional().describe("Company name (use with name)"),
|
|
39
117
|
email: z.string().optional().describe("Work email (optional disambiguator)"),
|
|
40
|
-
detail: z
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
detail: z.enum(["compact", "full"]).optional().describe("Response shape"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cleanBriefArgs(args) {
|
|
123
|
+
const body = {};
|
|
124
|
+
for (const key of ["linkedin_url", "name", "company", "email", "detail"])
|
|
125
|
+
if (args[key]) body[key] = args[key];
|
|
126
|
+
return body;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
server.tool(
|
|
130
|
+
"begin_programmatic_signup",
|
|
131
|
+
"Start named API-key setup. It sends an approval link to a human-controlled email. Do not infer approval or claim a key until the approval endpoint reports approved. If claimed, the key is kept only by this running MCP process; durable named-key setup belongs to a human in Account.",
|
|
132
|
+
{
|
|
133
|
+
email: z.string().email().describe("Human-controlled email address"),
|
|
134
|
+
device_name: z.string().max(120).optional().describe("Named device the human should recognize"),
|
|
44
135
|
},
|
|
45
136
|
async (args) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
137
|
+
const response = await api("/api/signup", {
|
|
138
|
+
auth: false,
|
|
139
|
+
body: { email: args.email, client_name: "Uniqueness MCP", device_name: args.device_name || "MCP host" },
|
|
140
|
+
});
|
|
141
|
+
if (response.status !== 202 || response.body.status !== "email_verification_required")
|
|
142
|
+
return json({
|
|
143
|
+
status: "signup_failed",
|
|
144
|
+
error: response.body.error || response.status,
|
|
145
|
+
...(response.body.error === "verification_delivery_failed"
|
|
146
|
+
? {
|
|
147
|
+
retry_after_seconds: response.body.retry_after_seconds || 60,
|
|
148
|
+
message:
|
|
149
|
+
"No key was created. Start a new named signup after the retry interval.",
|
|
150
|
+
}
|
|
151
|
+
: {}),
|
|
152
|
+
}, true);
|
|
153
|
+
if (
|
|
154
|
+
typeof response.body.transaction_id !== "string" ||
|
|
155
|
+
!response.body.transaction_id ||
|
|
156
|
+
typeof response.body.polling_token !== "string" ||
|
|
157
|
+
!response.body.polling_token
|
|
158
|
+
)
|
|
159
|
+
return json({ status: "signup_failed", error: "invalid_verification_response" }, true);
|
|
160
|
+
pruneSignupContinuations();
|
|
161
|
+
const continuationId = randomUUID();
|
|
162
|
+
const parsedExpiry = Date.parse(response.body.expires_at || "");
|
|
163
|
+
signupContinuations.set(continuationId, {
|
|
164
|
+
transactionId: response.body.transaction_id,
|
|
165
|
+
pollingToken: response.body.polling_token,
|
|
166
|
+
claimAttempted: false,
|
|
167
|
+
expiresAt: Number.isFinite(parsedExpiry)
|
|
168
|
+
? parsedExpiry
|
|
169
|
+
: Date.now() + 15 * 60 * 1000,
|
|
170
|
+
});
|
|
171
|
+
return json({
|
|
172
|
+
status: "human_action_required",
|
|
173
|
+
action: "email_approval",
|
|
174
|
+
message: "Ask the human to approve the named MCP/device request in their email. Approval is not inferred from this tool call.",
|
|
175
|
+
continuation_id: continuationId,
|
|
176
|
+
expires_at: response.body.expires_at,
|
|
177
|
+
complete_tool: "complete_programmatic_signup",
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.tool(
|
|
183
|
+
"complete_programmatic_signup",
|
|
184
|
+
"Check the in-process named email-approval request and claim a one-time key only after the server reports approved. The polling secret and key never appear in MCP output. A successful claim configures this running process only; a human must use Account for durable named-key setup.",
|
|
185
|
+
{ continuation_id: z.string().uuid() },
|
|
186
|
+
async ({ continuation_id }) => {
|
|
187
|
+
pruneSignupContinuations();
|
|
188
|
+
const continuation = signupContinuations.get(continuation_id);
|
|
189
|
+
if (!continuation)
|
|
190
|
+
return json({
|
|
191
|
+
status: "verification_expired",
|
|
192
|
+
message:
|
|
193
|
+
"This process no longer holds that verification. Start a new named signup.",
|
|
194
|
+
});
|
|
195
|
+
if (continuation.claimAttempted)
|
|
196
|
+
return json({
|
|
197
|
+
status: "claim_already_attempted",
|
|
198
|
+
human_action_required: true,
|
|
199
|
+
action: "account_key_setup",
|
|
200
|
+
account_url: `${BASE}/account`,
|
|
201
|
+
message:
|
|
202
|
+
"To preserve the one-time claim boundary, this process will not call claim again. A human can sign in to Account for durable named-key setup.",
|
|
203
|
+
});
|
|
204
|
+
const path = `/api/signup/verification/${encodeURIComponent(continuation.transactionId)}`;
|
|
205
|
+
const headers = { "x-verification-token": continuation.pollingToken };
|
|
206
|
+
const poll = await fetch(`${BASE}${path}`, { headers });
|
|
207
|
+
const state = await poll.json().catch(() => ({}));
|
|
208
|
+
if (state.status !== "approved") {
|
|
209
|
+
const result = json({
|
|
210
|
+
status: state.status || "pending",
|
|
211
|
+
human_action_required: state.status === "pending",
|
|
212
|
+
message: state.status === "pending" ? "Approval is still pending. Wait for the human email action." : "This approval request cannot issue a key. Start a new named signup if needed.",
|
|
213
|
+
});
|
|
214
|
+
if (state.status && state.status !== "pending")
|
|
215
|
+
signupContinuations.delete(continuation_id);
|
|
216
|
+
return result;
|
|
51
217
|
}
|
|
52
|
-
|
|
53
|
-
|
|
218
|
+
// The service intentionally makes this a one-time disclosure. Do not retry a
|
|
219
|
+
// claim after an ambiguous response: the key could have been issued, and only
|
|
220
|
+
// a human Account session is appropriate for durable recovery/setup.
|
|
221
|
+
continuation.claimAttempted = true;
|
|
222
|
+
let issued;
|
|
223
|
+
try {
|
|
224
|
+
const claim = await fetch(`${BASE}${path}/claim`, { method: "POST", headers });
|
|
225
|
+
issued = await claim.json().catch(() => ({}));
|
|
226
|
+
} catch {
|
|
227
|
+
return json({
|
|
228
|
+
status: "claim_uncertain",
|
|
229
|
+
human_action_required: true,
|
|
230
|
+
action: "account_key_setup",
|
|
231
|
+
account_url: `${BASE}/account`,
|
|
232
|
+
message:
|
|
233
|
+
"The one-time claim response was not received. It will not be repeated here; a human can sign in to Account for durable named-key setup.",
|
|
234
|
+
}, true);
|
|
54
235
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
236
|
+
if (issued.status !== "approved" || !issued.api_key)
|
|
237
|
+
return json({
|
|
238
|
+
status: "claim_failed",
|
|
239
|
+
error: issued.status || "unknown",
|
|
240
|
+
human_action_required: true,
|
|
241
|
+
action: "account_key_setup",
|
|
242
|
+
account_url: `${BASE}/account`,
|
|
243
|
+
message:
|
|
244
|
+
"The one-time claim was not configured in this process. It will not be repeated here; a human can sign in to Account for durable named-key setup.",
|
|
245
|
+
}, true);
|
|
246
|
+
KEY = issued.api_key;
|
|
247
|
+
signupContinuations.delete(continuation_id);
|
|
248
|
+
return json({
|
|
249
|
+
status: "configured",
|
|
250
|
+
scope: "current_process_only",
|
|
251
|
+
account_url: `${BASE}/account`,
|
|
252
|
+
message: "MCP is configured only for this currently running process. The key was received once and is intentionally never repeated in tool output. For a durable named key or a future MCP configuration, have the human sign in to Account; do not ask an agent to expose or relay the key.",
|
|
71
253
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (subJson.error === "insufficient_credits") {
|
|
87
|
-
return { isError: true, content: [{ type: "text", text: "Out of credits. Add credits or save a card with POST /api/billing/setup." }] };
|
|
88
|
-
}
|
|
89
|
-
return { isError: true, content: [{ type: "text", text: subJson.error || "Payment required." }] };
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
server.tool(
|
|
258
|
+
"get_connection_brief",
|
|
259
|
+
"Submit one full-quality written enrichment. A real target requires spendable credit. On canonical 402 this returns structured human_action_required; it never buys, saves a method, enables auto-recharge, or subscribes. Refusal, wrong-person, no-result, and system-failure outcomes release the exact credit. Support: support@uniquenessengine.com.",
|
|
260
|
+
briefInputSchema(),
|
|
261
|
+
async (args) => {
|
|
262
|
+
if (!KEY) return authenticationRequired();
|
|
263
|
+
if (!args.linkedin_url && !args.name) return json({ error: "invalid_input", message: "Provide linkedin_url, or name (+ company)." }, true);
|
|
264
|
+
const body = cleanBriefArgs(args);
|
|
265
|
+
const result = await submitBrief(body, randomUUID());
|
|
266
|
+
if (result.kind === "payment_required") {
|
|
267
|
+
return paymentAction(result.contract);
|
|
90
268
|
}
|
|
269
|
+
if (result.kind === "unauthorized") return json({ error: "unauthorized" }, true);
|
|
270
|
+
if (result.kind === "pending") return json({ status: "pending", job_id: result.job_id, message: "The job is still processing; call get_connection_brief again with the same input to poll a fresh server request." });
|
|
271
|
+
if (result.kind === "retry_rejected") return json({ error: result.error, message: "Submit a new request with a new idempotency key." }, true);
|
|
272
|
+
if (result.kind === "error") return json({ error: result.error, message: result.message }, true);
|
|
273
|
+
return json(result.brief);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
91
276
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
content: [{ type: "text", text: `Still processing (job ${jobId}). The brief is taking longer than usual — re-run get_connection_brief with the same inputs to keep polling.` }],
|
|
115
|
-
};
|
|
116
|
-
}
|
|
277
|
+
server.tool(
|
|
278
|
+
"resume_connection_brief",
|
|
279
|
+
"After a human has completed Checkout, verify spendable credit with the service and only then resubmit caller-supplied original target fields with the 402 retry idempotency key. The MCP server stores no unpaid target and never trusts a payment assertion alone.",
|
|
280
|
+
{
|
|
281
|
+
...briefInputSchema(),
|
|
282
|
+
idempotency_key: z.string().min(1).max(200).describe("payment_required.retry.idempotency_key from the original 402"),
|
|
283
|
+
previous_available: z.number().int().nonnegative().describe("payment_required.credits.available from the original 402"),
|
|
284
|
+
},
|
|
285
|
+
async (args) => {
|
|
286
|
+
if (!KEY) return authenticationRequired();
|
|
287
|
+
const body = cleanBriefArgs(args);
|
|
288
|
+
if (!body.linkedin_url && !body.name)
|
|
289
|
+
return json({ error: "invalid_input", message: "Resupply the original linkedin_url, or name (+ company). No unpaid target is stored by this MCP server." }, true);
|
|
290
|
+
const account = await api("/api/account", { method: "GET" });
|
|
291
|
+
const balance = spendableBalance(account.body);
|
|
292
|
+
if (account.status !== 200 || balance === null || balance <= args.previous_available) {
|
|
293
|
+
return json({
|
|
294
|
+
status: "human_action_required",
|
|
295
|
+
action: "verify_checkout",
|
|
296
|
+
message: "The service does not yet show more spendable credit than the original 402, so the caller-supplied target was not retried. Complete/check Checkout and call this tool again; do not claim payment as proof.",
|
|
297
|
+
observed_balance: balance,
|
|
298
|
+
});
|
|
117
299
|
}
|
|
300
|
+
const result = await submitBrief(body, args.idempotency_key);
|
|
301
|
+
if (result.kind === "payment_required") return paymentAction(result.contract);
|
|
302
|
+
if (result.kind === "pending") return json({ status: "pending", job_id: result.job_id });
|
|
303
|
+
if (result.kind === "retry_rejected")
|
|
304
|
+
return json({ error: result.error, message: "Retry metadata is no longer valid. Submit a new request with a new idempotency key." }, true);
|
|
305
|
+
if (result.kind === "error" || result.kind === "unauthorized") return json({ error: result.error || "unauthorized", message: result.message }, true);
|
|
306
|
+
return json(result.brief);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
118
309
|
|
|
119
|
-
|
|
120
|
-
|
|
310
|
+
server.tool(
|
|
311
|
+
"get_billing_status",
|
|
312
|
+
"Read spendable balance provenance and optional auto-recharge state. This is read-only; it never changes billing settings.",
|
|
313
|
+
{},
|
|
314
|
+
async () => {
|
|
315
|
+
if (!KEY) return authenticationRequired();
|
|
316
|
+
const [balance, recharge] = await Promise.all([
|
|
317
|
+
api("/api/account", { method: "GET" }),
|
|
318
|
+
api("/api/billing/auto-recharge", { method: "GET" }),
|
|
319
|
+
]);
|
|
320
|
+
if (balance.status !== 200) return json({ error: balance.body.error || "balance_unavailable" }, true);
|
|
321
|
+
return json({
|
|
322
|
+
balance: balance.body,
|
|
323
|
+
auto_recharge: recharge.status === 200 ? recharge.body : { error: recharge.body.error || "unavailable" },
|
|
324
|
+
auto_recharge_note: "Auto-recharge is off by default. This MCP server cannot enable, retry, or disable it; a human must make an explicit choice in billing settings.",
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
server.tool(
|
|
330
|
+
"preflight_connection_briefs",
|
|
331
|
+
"Read-only batch estimate. It never purchases credit, triggers auto-recharge, reserves credit, or starts a partial batch. Use the API directly with explicit human approval to submit an all-or-nothing batch.",
|
|
332
|
+
{ profiles: z.array(z.object({ linkedin_url: z.string().optional(), name: z.string().optional(), company: z.string().optional(), email: z.string().optional() })).min(1).max(100) },
|
|
333
|
+
async ({ profiles }) => {
|
|
334
|
+
if (!KEY) return authenticationRequired();
|
|
335
|
+
const response = await api("/api/enrich/batch", { body: { profiles, estimate_only: true } });
|
|
336
|
+
if (response.status !== 200) return json({ error: response.body.error || `http_${response.status}`, message: response.body.message }, true);
|
|
337
|
+
return json({ status: "preflight", ...response.body, all_or_nothing: true });
|
|
338
|
+
},
|
|
121
339
|
);
|
|
122
340
|
|
|
123
341
|
server.tool(
|
|
124
342
|
"submit_feedback",
|
|
125
|
-
"Rate a Connection Brief
|
|
126
|
-
"from the brief's identity (or the job_id if you have one) plus rating 'up' or 'down' " +
|
|
127
|
-
"and an optional note on what was right or wrong.",
|
|
343
|
+
"Rate a delivered Connection Brief. Feedback never changes billing.",
|
|
128
344
|
{
|
|
129
|
-
rating: z.enum(["up", "down"])
|
|
130
|
-
canonical_id: z
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.describe("brief.identity.canonical_id (preferred)"),
|
|
134
|
-
job_id: z.string().optional().describe("job id, if you enriched async"),
|
|
135
|
-
note: z.string().optional().describe("what was right/wrong (optional)"),
|
|
345
|
+
rating: z.enum(["up", "down"]),
|
|
346
|
+
canonical_id: z.string().optional(),
|
|
347
|
+
job_id: z.string().optional(),
|
|
348
|
+
note: z.string().optional(),
|
|
136
349
|
},
|
|
137
350
|
async (args) => {
|
|
138
|
-
if (!KEY)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
if (!args.canonical_id && !args.job_id) {
|
|
145
|
-
return {
|
|
146
|
-
isError: true,
|
|
147
|
-
content: [
|
|
148
|
-
{ type: "text", text: "Provide canonical_id (or job_id) to rate." },
|
|
149
|
-
],
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
const res = await fetch(`${BASE}/api/feedback`, {
|
|
153
|
-
method: "POST",
|
|
154
|
-
headers: {
|
|
155
|
-
"content-type": "application/json",
|
|
156
|
-
authorization: `Bearer ${KEY}`,
|
|
157
|
-
},
|
|
158
|
-
body: JSON.stringify({ ...args, source: "mcp" }),
|
|
159
|
-
});
|
|
160
|
-
const json = await res.json().catch(() => ({}));
|
|
161
|
-
if (!res.ok)
|
|
162
|
-
return {
|
|
163
|
-
isError: true,
|
|
164
|
-
content: [
|
|
165
|
-
{
|
|
166
|
-
type: "text",
|
|
167
|
-
text: `feedback failed (${res.status}): ${json.error || ""}`,
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
};
|
|
171
|
-
return { content: [{ type: "text", text: "Thanks — feedback recorded." }] };
|
|
351
|
+
if (!KEY) return authenticationRequired();
|
|
352
|
+
if (!args.canonical_id && !args.job_id) return json({ error: "invalid_input", message: "Provide canonical_id or job_id." }, true);
|
|
353
|
+
const response = await api("/api/feedback", { body: { ...args, source: "mcp" } });
|
|
354
|
+
if (response.status !== 200) return json({ error: response.body.error || `http_${response.status}` }, true);
|
|
355
|
+
return json({ status: "recorded", message: "Feedback recorded." });
|
|
172
356
|
},
|
|
173
357
|
);
|
|
174
358
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniqueness-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Uniqueness Engine
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Conservative human-in-the-loop MCP server for Uniqueness Engine fact-checked connection briefs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"uniqueness-mcp": "index.mjs"
|