uniqueness-mcp 0.5.2 → 0.7.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 +14 -42
- package/dist/server.js +491 -0
- package/index.mjs +16 -393
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
# uniqueness-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Local stdio fallback for the hosted [Uniqueness Engine](https://uniquenessengine.com) MCP server.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Prefer the hosted MCP
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Connect remote-capable clients to:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
`https://uniquenessengine.com/mcp`
|
|
10
|
+
|
|
11
|
+
Send `Authorization: Bearer uq_live_...` on every request. Hosted compatible fixes arrive without reinstalling this package.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Local stdio fallback
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Use this package only when your client cannot use remote Streamable HTTP or cannot set a bearer header:
|
|
16
16
|
|
|
17
17
|
```json
|
|
18
18
|
{
|
|
19
19
|
"mcpServers": {
|
|
20
20
|
"uniqueness": {
|
|
21
21
|
"command": "npx",
|
|
22
|
-
"args": ["-y", "uniqueness-mcp"],
|
|
22
|
+
"args": ["-y", "uniqueness-mcp@latest"],
|
|
23
23
|
"env": {
|
|
24
24
|
"UNIQUENESS_API_KEY": "uq_live_..."
|
|
25
25
|
}
|
|
@@ -28,40 +28,12 @@ Requires Node **18+**.
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Tools
|
|
34
|
-
|
|
35
|
-
| Tool | Purpose |
|
|
36
|
-
|------|---------|
|
|
37
|
-
| `get_personal_context` | Submit one enrichment (`detail=compact` default). Alias: `get_connection_brief` |
|
|
38
|
-
| `resume_personal_context` | After human Checkout, retry with 402 idempotency key. Alias: `resume_connection_brief` |
|
|
39
|
-
| `get_billing_status` | Read spendable balance + auto-recharge state (read-only) |
|
|
40
|
-
| `begin_programmatic_signup` / `complete_programmatic_signup` | Named-device verification flow |
|
|
41
|
-
| `preflight_personal_contexts` | Batch estimate before spend. Alias: `preflight_connection_briefs` |
|
|
42
|
-
| `submit_feedback` | Rate a delivered brief |
|
|
43
|
-
|
|
44
|
-
## Safety (by design)
|
|
31
|
+
Requires Node 18+.
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
- **`402 payment_required`** returns structured `human_action_required`; present `recommended_offer.checkout_url` or `billing_url` only after explicit human consent.
|
|
48
|
-
- **No unpaid target storage** — after Checkout, the host must call `resume_personal_context` with the original target fields, `payment_required.retry.idempotency_key`, and the previously observed available balance.
|
|
49
|
-
|
|
50
|
-
Jordan Ellis is a **fictional static fixture** and the only free experience (`GET /api/examples/kitchen-sink`). Do not smoke-test real people without explicit human approval and paid credit.
|
|
51
|
-
|
|
52
|
-
## Environment
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
export UNIQUENESS_API_KEY=uq_live_...
|
|
56
|
-
export UNIQUENESS_API_URL=https://uniquenessengine.com # optional override
|
|
57
|
-
```
|
|
33
|
+
The fallback and hosted endpoint use the same tool core. The fallback additionally exposes `begin_programmatic_signup` and `complete_programmatic_signup`; after human email approval, they configure only the current process and never return a key to the agent.
|
|
58
34
|
|
|
59
|
-
|
|
35
|
+
Lifecycle tools include `get_personal_context`, `get_connection_brief_job`, `batch_connection_briefs`, `get_batch_status`, `get_batch_results`, `export_batch`, `cancel_job`, `retry_job`, `retry_failed_batch`, and `estimate_batch_cost`.
|
|
60
36
|
|
|
61
|
-
-
|
|
62
|
-
- [Agent setup](https://uniquenessengine.com/agents)
|
|
63
|
-
- [llms.txt](https://uniquenessengine.com/llms.txt)
|
|
64
|
-
- [OpenAPI](https://uniquenessengine.com/openapi.json)
|
|
65
|
-
- [CLI](https://www.npmjs.com/package/uniqueness)
|
|
37
|
+
MCP has no purchase, payment-method, auto-recharge, or subscription tools. On `402 payment_required`, present checkout URLs only after explicit human consent.
|
|
66
38
|
|
|
67
|
-
|
|
39
|
+
[MCP quickstart](https://uniquenessengine.com/documentation/mcp/quickstart) · support@uniquenessengine.com
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const MCP_SERVER_NAME = "uniqueness";
|
|
5
|
+
export const MCP_SERVER_VERSION = "0.7.0";
|
|
6
|
+
export const MCP_TOOL_ALIASES = {
|
|
7
|
+
get_personal_context: ["get_connection_brief"],
|
|
8
|
+
resume_personal_context: ["resume_connection_brief"],
|
|
9
|
+
preflight_personal_contexts: ["preflight_connection_briefs"],
|
|
10
|
+
};
|
|
11
|
+
const CORE_TOOL_NAMES = [
|
|
12
|
+
"get_personal_context",
|
|
13
|
+
"get_connection_brief",
|
|
14
|
+
"resume_personal_context",
|
|
15
|
+
"resume_connection_brief",
|
|
16
|
+
"get_billing_status",
|
|
17
|
+
"preflight_personal_contexts",
|
|
18
|
+
"preflight_connection_briefs",
|
|
19
|
+
"get_connection_brief_job",
|
|
20
|
+
"batch_connection_briefs",
|
|
21
|
+
"estimate_batch_cost",
|
|
22
|
+
"get_batch_status",
|
|
23
|
+
"get_batch_results",
|
|
24
|
+
"export_batch",
|
|
25
|
+
"cancel_job",
|
|
26
|
+
"retry_job",
|
|
27
|
+
"retry_failed_batch",
|
|
28
|
+
"submit_feedback",
|
|
29
|
+
];
|
|
30
|
+
export const MCP_REMOTE_TOOL_NAMES = [...CORE_TOOL_NAMES];
|
|
31
|
+
export const MCP_STDIO_TOOL_NAMES = [
|
|
32
|
+
"begin_programmatic_signup",
|
|
33
|
+
"complete_programmatic_signup",
|
|
34
|
+
...CORE_TOOL_NAMES,
|
|
35
|
+
];
|
|
36
|
+
const json = (value, isError = false) => ({
|
|
37
|
+
...(isError ? { isError: true } : {}),
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
39
|
+
});
|
|
40
|
+
function isPaymentRequired(value) {
|
|
41
|
+
return (value?.error === "payment_required" &&
|
|
42
|
+
value?.reason === "insufficient_credits" &&
|
|
43
|
+
value?.credits &&
|
|
44
|
+
value?.retry &&
|
|
45
|
+
typeof value?.billing_url === "string");
|
|
46
|
+
}
|
|
47
|
+
function spendableBalance(value) {
|
|
48
|
+
const candidates = [
|
|
49
|
+
value?.total_credits,
|
|
50
|
+
value?.credits_remaining,
|
|
51
|
+
value?.balance?.total_credits,
|
|
52
|
+
value?.balance?.spendable_credits,
|
|
53
|
+
];
|
|
54
|
+
return (candidates.find((candidate) => typeof candidate === "number" && Number.isFinite(candidate)) ?? null);
|
|
55
|
+
}
|
|
56
|
+
function paymentAction(contract, batch = false) {
|
|
57
|
+
return json({
|
|
58
|
+
status: "human_action_required",
|
|
59
|
+
action: "checkout",
|
|
60
|
+
message: batch
|
|
61
|
+
? "Ask a human before presenting a purchase action. After explicit consent, use the offered checkout URL or billing URL, then resubmit the unchanged batch with the same idempotency key."
|
|
62
|
+
: "Ask a human before presenting a purchase action. After explicit consent, use the offered checkout URL or billing URL, then call resume_personal_context with the original target and retry fields.",
|
|
63
|
+
payment_required: contract,
|
|
64
|
+
resume_tool: batch ? "batch_connection_briefs" : "resume_personal_context",
|
|
65
|
+
...(batch
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
resume_tool_alias: MCP_TOOL_ALIASES.resume_personal_context[0],
|
|
69
|
+
}),
|
|
70
|
+
resume_requires: batch
|
|
71
|
+
? [
|
|
72
|
+
"original requests and options",
|
|
73
|
+
"original callback configuration, if any",
|
|
74
|
+
"payment_required.retry.idempotency_key",
|
|
75
|
+
]
|
|
76
|
+
: [
|
|
77
|
+
"original target fields",
|
|
78
|
+
"payment_required.retry.idempotency_key",
|
|
79
|
+
"payment_required.credits.available",
|
|
80
|
+
],
|
|
81
|
+
fixture: contract.fixture,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const profileSchema = z.object({
|
|
85
|
+
request_id: z.string().optional(),
|
|
86
|
+
linkedin_url: z.string().optional(),
|
|
87
|
+
name: z.string().optional(),
|
|
88
|
+
company: z.string().optional(),
|
|
89
|
+
email: z.string().optional(),
|
|
90
|
+
});
|
|
91
|
+
const detailSchema = z.enum(["compact", "full"]).optional();
|
|
92
|
+
const batchOptionsSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
detail: detailSchema,
|
|
95
|
+
public_sources_only: z.boolean().optional(),
|
|
96
|
+
include_sensitive_topics: z.boolean().optional(),
|
|
97
|
+
store_raw_quotes: z.boolean().optional(),
|
|
98
|
+
include_outreach_angles: z.boolean().optional(),
|
|
99
|
+
})
|
|
100
|
+
.optional();
|
|
101
|
+
const briefShape = {
|
|
102
|
+
linkedin_url: z.string().optional().describe("LinkedIn profile URL"),
|
|
103
|
+
name: z.string().optional().describe("Full name (use with company)"),
|
|
104
|
+
company: z.string().optional().describe("Company name (use with name)"),
|
|
105
|
+
email: z.string().optional().describe("Work email (optional disambiguator)"),
|
|
106
|
+
detail: detailSchema.describe('Response shape. Defaults to "compact"; use "full" for provenance sub-scores.'),
|
|
107
|
+
};
|
|
108
|
+
function cleanBriefArgs(args) {
|
|
109
|
+
const body = {};
|
|
110
|
+
for (const key of ["linkedin_url", "name", "company", "email", "detail"])
|
|
111
|
+
if (args[key])
|
|
112
|
+
body[key] = args[key];
|
|
113
|
+
if (!body.detail)
|
|
114
|
+
body.detail = "compact";
|
|
115
|
+
return body;
|
|
116
|
+
}
|
|
117
|
+
export function createMcpServer(context) {
|
|
118
|
+
const baseUrl = (context.baseUrl || "https://uniquenessengine.com").replace(/\/$/, "");
|
|
119
|
+
const signupContinuations = new Map();
|
|
120
|
+
const pruneSignupContinuations = () => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
for (const [id, continuation] of signupContinuations)
|
|
123
|
+
if (continuation.expiresAt <= now)
|
|
124
|
+
signupContinuations.delete(id);
|
|
125
|
+
while (signupContinuations.size > 100)
|
|
126
|
+
signupContinuations.delete(signupContinuations.keys().next().value);
|
|
127
|
+
};
|
|
128
|
+
const api = async (path, options = {}) => {
|
|
129
|
+
const auth = options.auth !== false;
|
|
130
|
+
const key = context.getApiKey();
|
|
131
|
+
if (auth && !key)
|
|
132
|
+
return { status: 401, body: { error: "missing_api_key" } };
|
|
133
|
+
const headers = {};
|
|
134
|
+
if (options.body !== undefined)
|
|
135
|
+
headers["content-type"] = "application/json";
|
|
136
|
+
if (auth)
|
|
137
|
+
headers.authorization = `Bearer ${key}`;
|
|
138
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
139
|
+
method: options.method || (options.body === undefined ? "GET" : "POST"),
|
|
140
|
+
headers,
|
|
141
|
+
...(options.body === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { body: JSON.stringify(options.body) }),
|
|
144
|
+
});
|
|
145
|
+
const contentType = response.headers.get("content-type") || "";
|
|
146
|
+
const body = contentType.includes("json")
|
|
147
|
+
? await response.json().catch(() => ({}))
|
|
148
|
+
: { text: await response.text() };
|
|
149
|
+
return { status: response.status, body };
|
|
150
|
+
};
|
|
151
|
+
const authenticationRequired = () => json({
|
|
152
|
+
status: "human_action_required",
|
|
153
|
+
action: context.allowSignup ? "programmatic_signup" : "configure_api_key",
|
|
154
|
+
message: context.allowSignup
|
|
155
|
+
? "No API key is configured. Use begin_programmatic_signup."
|
|
156
|
+
: "Configure the hosted MCP Authorization header with a Uniqueness Engine API key.",
|
|
157
|
+
...(context.allowSignup
|
|
158
|
+
? { begin_tool: "begin_programmatic_signup" }
|
|
159
|
+
: { account_url: `${baseUrl}/account` }),
|
|
160
|
+
}, true);
|
|
161
|
+
const requireKey = () => !!context.getApiKey();
|
|
162
|
+
const server = new McpServer({
|
|
163
|
+
name: MCP_SERVER_NAME,
|
|
164
|
+
version: MCP_SERVER_VERSION,
|
|
165
|
+
});
|
|
166
|
+
if (context.allowSignup) {
|
|
167
|
+
server.registerTool("begin_programmatic_signup", {
|
|
168
|
+
description: "Start named API-key setup. Human email approval is required.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
email: z.string().email(),
|
|
171
|
+
device_name: z.string().max(120).optional(),
|
|
172
|
+
},
|
|
173
|
+
}, async ({ email, device_name }) => {
|
|
174
|
+
pruneSignupContinuations();
|
|
175
|
+
const response = await api("/api/signup", {
|
|
176
|
+
auth: false,
|
|
177
|
+
body: {
|
|
178
|
+
email,
|
|
179
|
+
client_name: "Uniqueness MCP",
|
|
180
|
+
device_name: device_name || "MCP host",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (response.status !== 202 ||
|
|
184
|
+
response.body.status !== "email_verification_required" ||
|
|
185
|
+
typeof response.body.transaction_id !== "string" ||
|
|
186
|
+
typeof response.body.polling_token !== "string")
|
|
187
|
+
return json({
|
|
188
|
+
status: "signup_failed",
|
|
189
|
+
error: response.body.error || response.status,
|
|
190
|
+
retry_after_seconds: response.body.retry_after_seconds,
|
|
191
|
+
}, true);
|
|
192
|
+
const continuationId = randomUUID();
|
|
193
|
+
const parsedExpiry = Date.parse(response.body.expires_at || "");
|
|
194
|
+
signupContinuations.set(continuationId, {
|
|
195
|
+
transactionId: response.body.transaction_id,
|
|
196
|
+
pollingToken: response.body.polling_token,
|
|
197
|
+
claimAttempted: false,
|
|
198
|
+
expiresAt: Number.isFinite(parsedExpiry)
|
|
199
|
+
? parsedExpiry
|
|
200
|
+
: Date.now() + 15 * 60 * 1000,
|
|
201
|
+
});
|
|
202
|
+
return json({
|
|
203
|
+
status: "human_action_required",
|
|
204
|
+
action: "email_approval",
|
|
205
|
+
continuation_id: continuationId,
|
|
206
|
+
expires_at: response.body.expires_at,
|
|
207
|
+
complete_tool: "complete_programmatic_signup",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
server.registerTool("complete_programmatic_signup", {
|
|
211
|
+
description: "Complete a named signup only after human email approval.",
|
|
212
|
+
inputSchema: { continuation_id: z.string().uuid() },
|
|
213
|
+
}, async ({ continuation_id }) => {
|
|
214
|
+
pruneSignupContinuations();
|
|
215
|
+
const continuation = signupContinuations.get(continuation_id);
|
|
216
|
+
if (!continuation || continuation.expiresAt <= Date.now())
|
|
217
|
+
return json({
|
|
218
|
+
status: "verification_expired",
|
|
219
|
+
message: "Start a new named signup.",
|
|
220
|
+
}, true);
|
|
221
|
+
if (continuation.claimAttempted)
|
|
222
|
+
return json({
|
|
223
|
+
status: "claim_already_attempted",
|
|
224
|
+
action: "account_key_setup",
|
|
225
|
+
account_url: `${baseUrl}/account`,
|
|
226
|
+
}, true);
|
|
227
|
+
const path = `/api/signup/verification/${encodeURIComponent(continuation.transactionId)}`;
|
|
228
|
+
const headers = { "x-verification-token": continuation.pollingToken };
|
|
229
|
+
const poll = await fetch(`${baseUrl}${path}`, { headers });
|
|
230
|
+
const state = await poll.json().catch(() => ({}));
|
|
231
|
+
if (state.status !== "approved") {
|
|
232
|
+
if (state.status && state.status !== "pending")
|
|
233
|
+
signupContinuations.delete(continuation_id);
|
|
234
|
+
return json({
|
|
235
|
+
status: state.status || "pending",
|
|
236
|
+
human_action_required: state.status === "pending",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
continuation.claimAttempted = true;
|
|
240
|
+
const claim = await fetch(`${baseUrl}${path}/claim`, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers,
|
|
243
|
+
});
|
|
244
|
+
const issued = await claim.json().catch(() => ({}));
|
|
245
|
+
if (!claim.ok || typeof issued.api_key !== "string")
|
|
246
|
+
return json({
|
|
247
|
+
status: "claim_failed",
|
|
248
|
+
action: "account_key_setup",
|
|
249
|
+
account_url: `${baseUrl}/account`,
|
|
250
|
+
}, true);
|
|
251
|
+
context.setApiKey?.(issued.api_key);
|
|
252
|
+
signupContinuations.delete(continuation_id);
|
|
253
|
+
return json({
|
|
254
|
+
status: "configured",
|
|
255
|
+
scope: "current_process_only",
|
|
256
|
+
account_url: `${baseUrl}/account`,
|
|
257
|
+
message: "This MCP process is configured. The API key was not exposed in tool output.",
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const submitBrief = async (args, idempotencyKey) => {
|
|
262
|
+
if (!requireKey())
|
|
263
|
+
return authenticationRequired();
|
|
264
|
+
const response = await api("/api/enrich", {
|
|
265
|
+
body: {
|
|
266
|
+
...cleanBriefArgs(args),
|
|
267
|
+
idempotency_key: idempotencyKey || randomUUID(),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
if (response.status === 402 && isPaymentRequired(response.body))
|
|
271
|
+
return paymentAction(response.body);
|
|
272
|
+
if (response.status >= 400)
|
|
273
|
+
return json(response.body, true);
|
|
274
|
+
if (response.body.personal_context || response.body.brief)
|
|
275
|
+
return json(response.body.personal_context || response.body.brief);
|
|
276
|
+
return json({
|
|
277
|
+
...response.body,
|
|
278
|
+
message: response.body.job_id
|
|
279
|
+
? "Call get_connection_brief_job with this job_id; do not resubmit the person."
|
|
280
|
+
: undefined,
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
const registerBrief = (name, description) => server.registerTool(name, { description, inputSchema: briefShape }, async (args) => submitBrief(args));
|
|
284
|
+
registerBrief("get_personal_context", "Submit one full-quality written enrichment. Returns an inline result or a job_id for get_connection_brief_job.");
|
|
285
|
+
registerBrief(MCP_TOOL_ALIASES.get_personal_context[0], "Deprecated alias for get_personal_context.");
|
|
286
|
+
const resumeShape = {
|
|
287
|
+
...briefShape,
|
|
288
|
+
idempotency_key: z.string(),
|
|
289
|
+
previous_available: z.number().int().min(0),
|
|
290
|
+
};
|
|
291
|
+
const registerResume = (name, description) => server.registerTool(name, { description, inputSchema: resumeShape }, async (args) => {
|
|
292
|
+
if (!requireKey())
|
|
293
|
+
return authenticationRequired();
|
|
294
|
+
const balance = await api("/api/account", { method: "GET" });
|
|
295
|
+
const available = spendableBalance(balance.body);
|
|
296
|
+
if (balance.status !== 200 || available === null)
|
|
297
|
+
return json({
|
|
298
|
+
status: "balance_unavailable",
|
|
299
|
+
message: "Could not verify spendable credit.",
|
|
300
|
+
}, true);
|
|
301
|
+
if (available <= args.previous_available)
|
|
302
|
+
return json({
|
|
303
|
+
status: "human_action_required",
|
|
304
|
+
action: "verify_checkout",
|
|
305
|
+
message: "Spendable credit has not increased since the 402 response.",
|
|
306
|
+
observed_balance: available,
|
|
307
|
+
});
|
|
308
|
+
return submitBrief(args, args.idempotency_key);
|
|
309
|
+
});
|
|
310
|
+
registerResume("resume_personal_context", "Verify increased spendable credit, then replay the original request with its idempotency key.");
|
|
311
|
+
registerResume(MCP_TOOL_ALIASES.resume_personal_context[0], "Deprecated alias for resume_personal_context.");
|
|
312
|
+
server.registerTool("get_billing_status", {
|
|
313
|
+
description: "Read spendable balance and auto-recharge state.",
|
|
314
|
+
inputSchema: {},
|
|
315
|
+
annotations: { readOnlyHint: true },
|
|
316
|
+
}, async () => {
|
|
317
|
+
if (!requireKey())
|
|
318
|
+
return authenticationRequired();
|
|
319
|
+
const [balance, recharge] = await Promise.all([
|
|
320
|
+
api("/api/account", { method: "GET" }),
|
|
321
|
+
api("/api/billing/auto-recharge", { method: "GET" }),
|
|
322
|
+
]);
|
|
323
|
+
return json({
|
|
324
|
+
balance: balance.body,
|
|
325
|
+
auto_recharge: recharge.status === 200
|
|
326
|
+
? recharge.body
|
|
327
|
+
: { error: recharge.body.error || "unavailable" },
|
|
328
|
+
}, balance.status !== 200);
|
|
329
|
+
});
|
|
330
|
+
const registerPreflight = (name, description) => server.registerTool(name, {
|
|
331
|
+
description,
|
|
332
|
+
inputSchema: { profiles: z.array(profileSchema).min(1).max(100) },
|
|
333
|
+
annotations: { readOnlyHint: true },
|
|
334
|
+
}, async ({ profiles }) => {
|
|
335
|
+
if (!requireKey())
|
|
336
|
+
return authenticationRequired();
|
|
337
|
+
const response = await api("/api/enrich/batch", {
|
|
338
|
+
body: { profiles, estimate_only: true },
|
|
339
|
+
});
|
|
340
|
+
return json({ status: "preflight", ...response.body, all_or_nothing: true }, response.status >= 400);
|
|
341
|
+
});
|
|
342
|
+
registerPreflight("preflight_personal_contexts", "Read-only batch estimate; starts no work.");
|
|
343
|
+
registerPreflight(MCP_TOOL_ALIASES.preflight_personal_contexts[0], "Deprecated alias for preflight_personal_contexts.");
|
|
344
|
+
server.registerTool("get_connection_brief_job", {
|
|
345
|
+
description: "Retrieve one asynchronous job and its result when terminal.",
|
|
346
|
+
inputSchema: { job_id: z.string(), detail: detailSchema },
|
|
347
|
+
annotations: { readOnlyHint: true },
|
|
348
|
+
}, async ({ job_id, detail }) => {
|
|
349
|
+
if (!requireKey())
|
|
350
|
+
return authenticationRequired();
|
|
351
|
+
const response = await api(`/api/jobs/${encodeURIComponent(job_id)}?detail=${detail || "full"}`, { method: "GET" });
|
|
352
|
+
return json(response.status < 400
|
|
353
|
+
? {
|
|
354
|
+
...response.body,
|
|
355
|
+
legacy_status: response.body.status,
|
|
356
|
+
status: response.body.lifecycle_status || response.body.status,
|
|
357
|
+
}
|
|
358
|
+
: response.body, response.status >= 400);
|
|
359
|
+
});
|
|
360
|
+
server.registerTool("batch_connection_briefs", {
|
|
361
|
+
description: "Submit up to 100 people as one durable batch; excess work remains queued.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
requests: z.array(profileSchema).min(1).max(100),
|
|
364
|
+
idempotency_key: z.string().optional(),
|
|
365
|
+
callback_url: z.string().url().optional(),
|
|
366
|
+
callback_secret: z.string().max(512).optional(),
|
|
367
|
+
options: batchOptionsSchema,
|
|
368
|
+
},
|
|
369
|
+
}, async (args) => {
|
|
370
|
+
if (!requireKey())
|
|
371
|
+
return authenticationRequired();
|
|
372
|
+
const response = await api("/api/enrich/batch", { body: args });
|
|
373
|
+
if (response.status === 402 && isPaymentRequired(response.body))
|
|
374
|
+
return paymentAction(response.body, true);
|
|
375
|
+
return json(response.body, response.status >= 400);
|
|
376
|
+
});
|
|
377
|
+
server.registerTool("estimate_batch_cost", {
|
|
378
|
+
description: "Estimate batch cost without reserving credit.",
|
|
379
|
+
inputSchema: {
|
|
380
|
+
requests: z.array(profileSchema).min(1).max(100),
|
|
381
|
+
options: batchOptionsSchema,
|
|
382
|
+
},
|
|
383
|
+
annotations: { readOnlyHint: true },
|
|
384
|
+
}, async ({ requests, options }) => {
|
|
385
|
+
if (!requireKey())
|
|
386
|
+
return authenticationRequired();
|
|
387
|
+
const response = await api("/api/enrich/batch", {
|
|
388
|
+
body: { requests, options, estimate_only: true },
|
|
389
|
+
});
|
|
390
|
+
return json(response.body, response.status >= 400);
|
|
391
|
+
});
|
|
392
|
+
server.registerTool("get_batch_status", {
|
|
393
|
+
description: "Retrieve aggregate and per-job batch status.",
|
|
394
|
+
inputSchema: { batch_id: z.string() },
|
|
395
|
+
annotations: { readOnlyHint: true },
|
|
396
|
+
}, async ({ batch_id }) => {
|
|
397
|
+
if (!requireKey())
|
|
398
|
+
return authenticationRequired();
|
|
399
|
+
const response = await api(`/api/enrich/batch/${encodeURIComponent(batch_id)}`, { method: "GET" });
|
|
400
|
+
return json(response.body, response.status >= 400);
|
|
401
|
+
});
|
|
402
|
+
server.registerTool("get_batch_results", {
|
|
403
|
+
description: "Retrieve a stable page of batch results, including failures and needs-review outcomes.",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
batch_id: z.string(),
|
|
406
|
+
cursor: z.string().optional(),
|
|
407
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
408
|
+
detail: detailSchema,
|
|
409
|
+
},
|
|
410
|
+
annotations: { readOnlyHint: true },
|
|
411
|
+
}, async ({ batch_id, cursor, limit, detail }) => {
|
|
412
|
+
if (!requireKey())
|
|
413
|
+
return authenticationRequired();
|
|
414
|
+
const query = new URLSearchParams({
|
|
415
|
+
cursor: cursor || "0",
|
|
416
|
+
limit: String(limit || 50),
|
|
417
|
+
detail: detail || "full",
|
|
418
|
+
});
|
|
419
|
+
const response = await api(`/api/enrich/batch/${encodeURIComponent(batch_id)}/results?${query}`, { method: "GET" });
|
|
420
|
+
return json(response.body, response.status >= 400);
|
|
421
|
+
});
|
|
422
|
+
server.registerTool("export_batch", {
|
|
423
|
+
description: "Export a terminal batch as JSON, JSONL, CSV, or Markdown.",
|
|
424
|
+
inputSchema: {
|
|
425
|
+
batch_id: z.string(),
|
|
426
|
+
format: z.enum(["json", "jsonl", "csv", "markdown"]).optional(),
|
|
427
|
+
},
|
|
428
|
+
annotations: { readOnlyHint: true },
|
|
429
|
+
}, async ({ batch_id, format }) => {
|
|
430
|
+
if (!requireKey())
|
|
431
|
+
return authenticationRequired();
|
|
432
|
+
const key = context.getApiKey();
|
|
433
|
+
const response = await fetch(`${baseUrl}/api/enrich/batch/${encodeURIComponent(batch_id)}/export?format=${format || "jsonl"}`, { headers: { authorization: `Bearer ${key}` } });
|
|
434
|
+
const text = await response.text();
|
|
435
|
+
return {
|
|
436
|
+
...(response.ok ? {} : { isError: true }),
|
|
437
|
+
content: [{ type: "text", text }],
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
const registerJobAction = (name, action) => server.registerTool(name, {
|
|
441
|
+
description: action === "cancel"
|
|
442
|
+
? "Cancel one queued or running job."
|
|
443
|
+
: "Retry one terminal system failure.",
|
|
444
|
+
inputSchema: { job_id: z.string() },
|
|
445
|
+
annotations: action === "cancel"
|
|
446
|
+
? { destructiveHint: true }
|
|
447
|
+
: { idempotentHint: true },
|
|
448
|
+
}, async ({ job_id }) => {
|
|
449
|
+
if (!requireKey())
|
|
450
|
+
return authenticationRequired();
|
|
451
|
+
const response = await api(`/api/jobs/${encodeURIComponent(job_id)}/${action}`, { body: {} });
|
|
452
|
+
return json(response.body, response.status >= 400);
|
|
453
|
+
});
|
|
454
|
+
registerJobAction("cancel_job", "cancel");
|
|
455
|
+
registerJobAction("retry_job", "retry");
|
|
456
|
+
server.registerTool("retry_failed_batch", {
|
|
457
|
+
description: "Submit a new batch containing only terminal system failures.",
|
|
458
|
+
inputSchema: {
|
|
459
|
+
batch_id: z.string(),
|
|
460
|
+
idempotency_key: z.string().optional(),
|
|
461
|
+
},
|
|
462
|
+
annotations: { idempotentHint: true },
|
|
463
|
+
}, async ({ batch_id, idempotency_key }) => {
|
|
464
|
+
if (!requireKey())
|
|
465
|
+
return authenticationRequired();
|
|
466
|
+
const response = await api(`/api/enrich/batch/${encodeURIComponent(batch_id)}/retry`, { body: { idempotency_key } });
|
|
467
|
+
return json(response.body, response.status >= 400);
|
|
468
|
+
});
|
|
469
|
+
server.registerTool("submit_feedback", {
|
|
470
|
+
description: "Rate a delivered personal context.",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
rating: z.enum(["up", "down"]),
|
|
473
|
+
canonical_id: z.string().optional(),
|
|
474
|
+
job_id: z.string().optional(),
|
|
475
|
+
note: z.string().optional(),
|
|
476
|
+
},
|
|
477
|
+
}, async (args) => {
|
|
478
|
+
if (!requireKey())
|
|
479
|
+
return authenticationRequired();
|
|
480
|
+
if (!args.canonical_id && !args.job_id)
|
|
481
|
+
return json({
|
|
482
|
+
error: "invalid_input",
|
|
483
|
+
message: "Provide canonical_id or job_id.",
|
|
484
|
+
}, true);
|
|
485
|
+
const response = await api("/api/feedback", {
|
|
486
|
+
body: { ...args, source: "mcp" },
|
|
487
|
+
});
|
|
488
|
+
return json(response.body, response.status >= 400);
|
|
489
|
+
});
|
|
490
|
+
return server;
|
|
491
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,398 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// saves a payment method, enables auto-recharge, or subscribes by inference.
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
// Local stdio fallback. Tool behavior lives in server.ts and is compiled into
|
|
3
|
+
// dist/ for this npm package; the hosted MCP imports the same source directly.
|
|
7
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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_personal_context (alias: 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_personal_context",
|
|
69
|
-
resume_tool_alias: "resume_connection_brief",
|
|
70
|
-
resume_requires: [
|
|
71
|
-
"original target fields",
|
|
72
|
-
"payment_required.retry.idempotency_key",
|
|
73
|
-
"payment_required.credits.available",
|
|
74
|
-
],
|
|
75
|
-
fixture: contract.fixture,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function authenticationRequired() {
|
|
80
|
-
return json({
|
|
81
|
-
status: "human_action_required",
|
|
82
|
-
action: "programmatic_signup",
|
|
83
|
-
message: "No API key is configured. Begin named email approval; do not use a key from another person or assume approval.",
|
|
84
|
-
begin_tool: "begin_programmatic_signup",
|
|
85
|
-
}, true);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function pollBrief(jobId) {
|
|
89
|
-
const deadline = Date.now() + 210000;
|
|
90
|
-
while (Date.now() < deadline) {
|
|
91
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
92
|
-
const poll = await api(`/api/jobs/${jobId}`, { method: "GET" });
|
|
93
|
-
if (poll.body.status === "done") return { kind: "brief", brief: poll.body.result };
|
|
94
|
-
if (poll.body.status === "refused") return { kind: "brief", brief: poll.body.result || { status: "needs_review", note: "Identity could not be confidently resolved." } };
|
|
95
|
-
if (poll.body.status === "failed") return { kind: "error", error: poll.body.error || "job_failed" };
|
|
96
|
-
}
|
|
97
|
-
return { kind: "pending", job_id: jobId };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function submitBrief(args, idempotencyKey) {
|
|
101
|
-
const payload = { ...args, idempotency_key: idempotencyKey };
|
|
102
|
-
const submit = await api("/api/enrich", { body: payload });
|
|
103
|
-
if (submit.status === 402 && isPaymentRequired(submit.body)) return { kind: "payment_required", contract: submit.body };
|
|
104
|
-
if (submit.status === 401) return { kind: "unauthorized" };
|
|
105
|
-
if (submit.status === 409 && (submit.body.error === "idempotency_key_expired" || submit.body.error === "idempotency_key_mismatch"))
|
|
106
|
-
return { kind: "retry_rejected", error: submit.body.error };
|
|
107
|
-
if (submit.status >= 400) return { kind: "error", error: submit.body.error || `http_${submit.status}`, message: submit.body.message };
|
|
108
|
-
if (submit.body.personal_context || submit.body.brief)
|
|
109
|
-
return { kind: "brief", brief: submit.body.personal_context || submit.body.brief };
|
|
110
|
-
if (!submit.body.job_id) return { kind: "error", error: "invalid_enrich_response" };
|
|
111
|
-
return pollBrief(submit.body.job_id);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function briefInputSchema() {
|
|
115
|
-
return {
|
|
116
|
-
linkedin_url: z.string().optional().describe("LinkedIn profile URL"),
|
|
117
|
-
name: z.string().optional().describe("Full name (use with company)"),
|
|
118
|
-
company: z.string().optional().describe("Company name (use with name)"),
|
|
119
|
-
email: z.string().optional().describe("Work email (optional disambiguator)"),
|
|
120
|
-
detail: z
|
|
121
|
-
.enum(["compact", "full"])
|
|
122
|
-
.optional()
|
|
123
|
-
.describe(
|
|
124
|
-
'Response shape. Defaults to "compact" (recommended for agents). Pass "full" for provenance sub-scores (debugging/eval/trust UI).',
|
|
125
|
-
),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function cleanBriefArgs(args) {
|
|
130
|
-
const body = {};
|
|
131
|
-
for (const key of ["linkedin_url", "name", "company", "email", "detail"])
|
|
132
|
-
if (args[key]) body[key] = args[key];
|
|
133
|
-
// Agents should not inherit the HTTP production default (full). Compact is the
|
|
134
|
-
// recommended agent path; omit only means "use MCP default", not "use server default".
|
|
135
|
-
if (!body.detail) body.detail = "compact";
|
|
136
|
-
return body;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
server.tool(
|
|
140
|
-
"begin_programmatic_signup",
|
|
141
|
-
"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.",
|
|
142
|
-
{
|
|
143
|
-
email: z.string().email().describe("Human-controlled email address"),
|
|
144
|
-
device_name: z.string().max(120).optional().describe("Named device the human should recognize"),
|
|
145
|
-
},
|
|
146
|
-
async (args) => {
|
|
147
|
-
const response = await api("/api/signup", {
|
|
148
|
-
auth: false,
|
|
149
|
-
body: { email: args.email, client_name: "Uniqueness MCP", device_name: args.device_name || "MCP host" },
|
|
150
|
-
});
|
|
151
|
-
if (response.status !== 202 || response.body.status !== "email_verification_required")
|
|
152
|
-
return json({
|
|
153
|
-
status: "signup_failed",
|
|
154
|
-
error: response.body.error || response.status,
|
|
155
|
-
...(response.body.error === "verification_delivery_failed"
|
|
156
|
-
? {
|
|
157
|
-
retry_after_seconds: response.body.retry_after_seconds || 60,
|
|
158
|
-
message:
|
|
159
|
-
"No key was created. Start a new named signup after the retry interval.",
|
|
160
|
-
}
|
|
161
|
-
: {}),
|
|
162
|
-
}, true);
|
|
163
|
-
if (
|
|
164
|
-
typeof response.body.transaction_id !== "string" ||
|
|
165
|
-
!response.body.transaction_id ||
|
|
166
|
-
typeof response.body.polling_token !== "string" ||
|
|
167
|
-
!response.body.polling_token
|
|
168
|
-
)
|
|
169
|
-
return json({ status: "signup_failed", error: "invalid_verification_response" }, true);
|
|
170
|
-
pruneSignupContinuations();
|
|
171
|
-
const continuationId = randomUUID();
|
|
172
|
-
const parsedExpiry = Date.parse(response.body.expires_at || "");
|
|
173
|
-
signupContinuations.set(continuationId, {
|
|
174
|
-
transactionId: response.body.transaction_id,
|
|
175
|
-
pollingToken: response.body.polling_token,
|
|
176
|
-
claimAttempted: false,
|
|
177
|
-
expiresAt: Number.isFinite(parsedExpiry)
|
|
178
|
-
? parsedExpiry
|
|
179
|
-
: Date.now() + 15 * 60 * 1000,
|
|
180
|
-
});
|
|
181
|
-
return json({
|
|
182
|
-
status: "human_action_required",
|
|
183
|
-
action: "email_approval",
|
|
184
|
-
message: "Ask the human to approve the named MCP/device request in their email. Approval is not inferred from this tool call.",
|
|
185
|
-
continuation_id: continuationId,
|
|
186
|
-
expires_at: response.body.expires_at,
|
|
187
|
-
complete_tool: "complete_programmatic_signup",
|
|
188
|
-
});
|
|
189
|
-
},
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
server.tool(
|
|
193
|
-
"complete_programmatic_signup",
|
|
194
|
-
"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.",
|
|
195
|
-
{ continuation_id: z.string().uuid() },
|
|
196
|
-
async ({ continuation_id }) => {
|
|
197
|
-
pruneSignupContinuations();
|
|
198
|
-
const continuation = signupContinuations.get(continuation_id);
|
|
199
|
-
if (!continuation)
|
|
200
|
-
return json({
|
|
201
|
-
status: "verification_expired",
|
|
202
|
-
message:
|
|
203
|
-
"This process no longer holds that verification. Start a new named signup.",
|
|
204
|
-
});
|
|
205
|
-
if (continuation.claimAttempted)
|
|
206
|
-
return json({
|
|
207
|
-
status: "claim_already_attempted",
|
|
208
|
-
human_action_required: true,
|
|
209
|
-
action: "account_key_setup",
|
|
210
|
-
account_url: `${BASE}/account`,
|
|
211
|
-
message:
|
|
212
|
-
"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.",
|
|
213
|
-
});
|
|
214
|
-
const path = `/api/signup/verification/${encodeURIComponent(continuation.transactionId)}`;
|
|
215
|
-
const headers = { "x-verification-token": continuation.pollingToken };
|
|
216
|
-
const poll = await fetch(`${BASE}${path}`, { headers });
|
|
217
|
-
const state = await poll.json().catch(() => ({}));
|
|
218
|
-
if (state.status !== "approved") {
|
|
219
|
-
const result = json({
|
|
220
|
-
status: state.status || "pending",
|
|
221
|
-
human_action_required: state.status === "pending",
|
|
222
|
-
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.",
|
|
223
|
-
});
|
|
224
|
-
if (state.status && state.status !== "pending")
|
|
225
|
-
signupContinuations.delete(continuation_id);
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
// The service intentionally makes this a one-time disclosure. Do not retry a
|
|
229
|
-
// claim after an ambiguous response: the key could have been issued, and only
|
|
230
|
-
// a human Account session is appropriate for durable recovery/setup.
|
|
231
|
-
continuation.claimAttempted = true;
|
|
232
|
-
let issued;
|
|
233
|
-
try {
|
|
234
|
-
const claim = await fetch(`${BASE}${path}/claim`, { method: "POST", headers });
|
|
235
|
-
issued = await claim.json().catch(() => ({}));
|
|
236
|
-
} catch {
|
|
237
|
-
return json({
|
|
238
|
-
status: "claim_uncertain",
|
|
239
|
-
human_action_required: true,
|
|
240
|
-
action: "account_key_setup",
|
|
241
|
-
account_url: `${BASE}/account`,
|
|
242
|
-
message:
|
|
243
|
-
"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.",
|
|
244
|
-
}, true);
|
|
245
|
-
}
|
|
246
|
-
if (issued.status !== "approved" || !issued.api_key)
|
|
247
|
-
return json({
|
|
248
|
-
status: "claim_failed",
|
|
249
|
-
error: issued.status || "unknown",
|
|
250
|
-
human_action_required: true,
|
|
251
|
-
action: "account_key_setup",
|
|
252
|
-
account_url: `${BASE}/account`,
|
|
253
|
-
message:
|
|
254
|
-
"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.",
|
|
255
|
-
}, true);
|
|
256
|
-
KEY = issued.api_key;
|
|
257
|
-
signupContinuations.delete(continuation_id);
|
|
258
|
-
return json({
|
|
259
|
-
status: "configured",
|
|
260
|
-
scope: "current_process_only",
|
|
261
|
-
account_url: `${BASE}/account`,
|
|
262
|
-
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.",
|
|
263
|
-
});
|
|
5
|
+
import { createMcpServer } from "./dist/server.js";
|
|
6
|
+
|
|
7
|
+
let apiKey = process.env.UNIQUENESS_API_KEY || "";
|
|
8
|
+
const server = createMcpServer({
|
|
9
|
+
baseUrl:
|
|
10
|
+
process.env.UNIQUENESS_API_URL || "https://uniquenessengine.com",
|
|
11
|
+
getApiKey: () => apiKey,
|
|
12
|
+
setApiKey: (value) => {
|
|
13
|
+
apiKey = value;
|
|
264
14
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
function registerGetPersonalContext(toolName, description) {
|
|
268
|
-
server.tool(toolName, description, briefInputSchema(), async (args) => {
|
|
269
|
-
if (!KEY) return authenticationRequired();
|
|
270
|
-
if (!args.linkedin_url && !args.name) return json({ error: "invalid_input", message: "Provide linkedin_url, or name (+ company)." }, true);
|
|
271
|
-
const body = cleanBriefArgs(args);
|
|
272
|
-
const result = await submitBrief(body, randomUUID());
|
|
273
|
-
if (result.kind === "payment_required") {
|
|
274
|
-
return paymentAction(result.contract);
|
|
275
|
-
}
|
|
276
|
-
if (result.kind === "unauthorized") return json({ error: "unauthorized" }, true);
|
|
277
|
-
if (result.kind === "pending") return json({ status: "pending", job_id: result.job_id, message: "The job is still processing; call get_personal_context again with the same input to poll a fresh server request." });
|
|
278
|
-
if (result.kind === "retry_rejected") return json({ error: result.error, message: "Submit a new request with a new idempotency key." }, true);
|
|
279
|
-
if (result.kind === "error") return json({ error: result.error, message: result.message }, true);
|
|
280
|
-
return json(result.brief);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function registerResumePersonalContext(toolName, description) {
|
|
285
|
-
server.tool(
|
|
286
|
-
toolName,
|
|
287
|
-
description,
|
|
288
|
-
{
|
|
289
|
-
...briefInputSchema(),
|
|
290
|
-
idempotency_key: z.string().min(1).max(200).describe("payment_required.retry.idempotency_key from the original 402"),
|
|
291
|
-
previous_available: z.number().int().nonnegative().describe("payment_required.credits.available from the original 402"),
|
|
292
|
-
},
|
|
293
|
-
async (args) => {
|
|
294
|
-
if (!KEY) return authenticationRequired();
|
|
295
|
-
const body = cleanBriefArgs(args);
|
|
296
|
-
if (!body.linkedin_url && !body.name)
|
|
297
|
-
return json({ error: "invalid_input", message: "Resupply the original linkedin_url, or name (+ company). No unpaid target is stored by this MCP server." }, true);
|
|
298
|
-
const account = await api("/api/account", { method: "GET" });
|
|
299
|
-
const balance = spendableBalance(account.body);
|
|
300
|
-
if (account.status !== 200 || balance === null || balance <= args.previous_available) {
|
|
301
|
-
return json({
|
|
302
|
-
status: "human_action_required",
|
|
303
|
-
action: "verify_checkout",
|
|
304
|
-
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.",
|
|
305
|
-
observed_balance: balance,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
const result = await submitBrief(body, args.idempotency_key);
|
|
309
|
-
if (result.kind === "payment_required") return paymentAction(result.contract);
|
|
310
|
-
if (result.kind === "pending") return json({ status: "pending", job_id: result.job_id });
|
|
311
|
-
if (result.kind === "retry_rejected")
|
|
312
|
-
return json({ error: result.error, message: "Retry metadata is no longer valid. Submit a new request with a new idempotency key." }, true);
|
|
313
|
-
if (result.kind === "error" || result.kind === "unauthorized") return json({ error: result.error || "unauthorized", message: result.message }, true);
|
|
314
|
-
return json(result.brief);
|
|
315
|
-
},
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function registerPreflightPersonalContexts(toolName, description) {
|
|
320
|
-
server.tool(
|
|
321
|
-
toolName,
|
|
322
|
-
description,
|
|
323
|
-
{ 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) },
|
|
324
|
-
async ({ profiles }) => {
|
|
325
|
-
if (!KEY) return authenticationRequired();
|
|
326
|
-
const response = await api("/api/enrich/batch", { body: { profiles, estimate_only: true } });
|
|
327
|
-
if (response.status !== 200) return json({ error: response.body.error || `http_${response.status}`, message: response.body.message }, true);
|
|
328
|
-
return json({ status: "preflight", ...response.body, all_or_nothing: true });
|
|
329
|
-
},
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
registerGetPersonalContext(
|
|
334
|
-
"get_personal_context",
|
|
335
|
-
"Submit one full-quality written enrichment (personal context). A real target requires spendable credit. Defaults to detail=compact (recommended for agents); pass detail=full for provenance sub-scores. 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.",
|
|
336
|
-
);
|
|
337
|
-
registerGetPersonalContext(
|
|
338
|
-
"get_connection_brief",
|
|
339
|
-
"Deprecated alias for get_personal_context. Prefer get_personal_context.",
|
|
340
|
-
);
|
|
341
|
-
registerResumePersonalContext(
|
|
342
|
-
"resume_personal_context",
|
|
343
|
-
"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.",
|
|
344
|
-
);
|
|
345
|
-
registerResumePersonalContext(
|
|
346
|
-
"resume_connection_brief",
|
|
347
|
-
"Deprecated alias for resume_personal_context. Prefer resume_personal_context.",
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
server.tool(
|
|
351
|
-
"get_billing_status",
|
|
352
|
-
"Read spendable balance provenance and optional auto-recharge state. This is read-only; it never changes billing settings.",
|
|
353
|
-
{},
|
|
354
|
-
async () => {
|
|
355
|
-
if (!KEY) return authenticationRequired();
|
|
356
|
-
const [balance, recharge] = await Promise.all([
|
|
357
|
-
api("/api/account", { method: "GET" }),
|
|
358
|
-
api("/api/billing/auto-recharge", { method: "GET" }),
|
|
359
|
-
]);
|
|
360
|
-
if (balance.status !== 200) return json({ error: balance.body.error || "balance_unavailable" }, true);
|
|
361
|
-
return json({
|
|
362
|
-
balance: balance.body,
|
|
363
|
-
auto_recharge: recharge.status === 200 ? recharge.body : { error: recharge.body.error || "unavailable" },
|
|
364
|
-
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.",
|
|
365
|
-
});
|
|
366
|
-
},
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
registerPreflightPersonalContexts(
|
|
370
|
-
"preflight_personal_contexts",
|
|
371
|
-
"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.",
|
|
372
|
-
);
|
|
373
|
-
registerPreflightPersonalContexts(
|
|
374
|
-
"preflight_connection_briefs",
|
|
375
|
-
"Deprecated alias for preflight_personal_contexts. Prefer preflight_personal_contexts.",
|
|
376
|
-
);
|
|
15
|
+
allowSignup: true,
|
|
16
|
+
});
|
|
377
17
|
|
|
378
|
-
server.
|
|
379
|
-
|
|
380
|
-
"
|
|
381
|
-
{
|
|
382
|
-
rating: z.enum(["up", "down"]),
|
|
383
|
-
canonical_id: z.string().optional(),
|
|
384
|
-
job_id: z.string().optional(),
|
|
385
|
-
note: z.string().optional(),
|
|
386
|
-
},
|
|
387
|
-
async (args) => {
|
|
388
|
-
if (!KEY) return authenticationRequired();
|
|
389
|
-
if (!args.canonical_id && !args.job_id) return json({ error: "invalid_input", message: "Provide canonical_id or job_id." }, true);
|
|
390
|
-
const response = await api("/api/feedback", { body: { ...args, source: "mcp" } });
|
|
391
|
-
if (response.status !== 200) return json({ error: response.body.error || `http_${response.status}` }, true);
|
|
392
|
-
return json({ status: "recorded", message: "Feedback recorded." });
|
|
393
|
-
},
|
|
18
|
+
await server.connect(new StdioServerTransport());
|
|
19
|
+
console.error(
|
|
20
|
+
"uniqueness-mcp ready (stdio; support@uniquenessengine.com)",
|
|
394
21
|
);
|
|
395
|
-
|
|
396
|
-
const transport = new StdioServerTransport();
|
|
397
|
-
await server.connect(transport);
|
|
398
|
-
console.error("uniqueness-mcp ready (stdio)");
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniqueness-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Local stdio fallback for the hosted Uniqueness Engine MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"uniqueness-mcp": "index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.mjs",
|
|
11
|
+
"dist",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"prepack": "npm run build"
|
|
17
|
+
},
|
|
13
18
|
"engines": {
|
|
14
19
|
"node": ">=18"
|
|
15
20
|
},
|
|
@@ -17,5 +22,8 @@
|
|
|
17
22
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
18
23
|
"zod": "^3.23.0"
|
|
19
24
|
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.9.2"
|
|
27
|
+
},
|
|
20
28
|
"license": "MIT"
|
|
21
29
|
}
|