@namingsignal/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/agent-guidance.js +7 -0
- package/dist/core/batch.js +142 -0
- package/dist/core/brief-constraints.js +72 -0
- package/dist/core/brief.js +402 -0
- package/dist/core/candidate-sort.js +47 -0
- package/dist/core/candidates.js +210 -0
- package/dist/core/domain.js +607 -0
- package/dist/core/editorial-review.js +27 -0
- package/dist/core/export.js +235 -0
- package/dist/core/import-safety.js +298 -0
- package/dist/core/index.js +31 -0
- package/dist/core/namesake.js +53 -0
- package/dist/core/namespaces.js +295 -0
- package/dist/core/normalize.js +75 -0
- package/dist/core/scoring.js +409 -0
- package/dist/core/types.js +3 -0
- package/dist/core/url-input.js +28 -0
- package/dist/core/usage.js +271 -0
- package/dist/index.js +19 -0
- package/dist/server-core.js +418 -0
- package/package.json +48 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NAMING_SIGNAL_TOOL_NAMES = exports.DEFAULT_API_URL = void 0;
|
|
4
|
+
exports.createNamingSignalMcpServer = createNamingSignalMcpServer;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
|
+
const v4_1 = require("zod/v4");
|
|
8
|
+
const agent_guidance_1 = require("./agent-guidance");
|
|
9
|
+
const batch_1 = require("./core/batch");
|
|
10
|
+
const index_1 = require("./core/index");
|
|
11
|
+
exports.DEFAULT_API_URL = "https://namingsignal.com";
|
|
12
|
+
const DEFAULT_TLDS = ["com", "io", "dev", "app"];
|
|
13
|
+
/** The complete tool surface, exported so tests and manifests stay in sync. */
|
|
14
|
+
exports.NAMING_SIGNAL_TOOL_NAMES = [
|
|
15
|
+
"compile_project_brief",
|
|
16
|
+
"create_naming_sprint",
|
|
17
|
+
"check_names",
|
|
18
|
+
"analyze_candidates",
|
|
19
|
+
"get_purchase_links",
|
|
20
|
+
];
|
|
21
|
+
function requestId(prefix) {
|
|
22
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
23
|
+
}
|
|
24
|
+
function stableStringify(value) {
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
27
|
+
if (value && typeof value === "object") {
|
|
28
|
+
return `{${Object.entries(value)
|
|
29
|
+
.filter(([, entry]) => entry !== undefined)
|
|
30
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
31
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`)
|
|
32
|
+
.join(",")}}`;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(value) ?? "null";
|
|
35
|
+
}
|
|
36
|
+
function mergeUsage(current, next) {
|
|
37
|
+
if (!next)
|
|
38
|
+
return current;
|
|
39
|
+
for (const [key, value] of Object.entries(next)) {
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
41
|
+
current[key] = (current[key] ?? 0) + value;
|
|
42
|
+
}
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
// Single-copy payloads: compact JSON in one text block, no structuredContent
|
|
46
|
+
// duplication and no pretty-print indentation.
|
|
47
|
+
function toolResult(result) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function compactDomainLine(domain) {
|
|
53
|
+
const tld = domain.domain?.includes(".") ? domain.domain.slice(domain.domain.indexOf(".") + 1) : domain.domain;
|
|
54
|
+
const provider = domain.provider || domain.evidence?.provider || "unknown";
|
|
55
|
+
const checkedAt = String(domain.checkedAt ?? domain.evidence?.checkedAt ?? "").slice(0, 10);
|
|
56
|
+
return `${tld}: ${domain.status} (${provider}${checkedAt ? `, ${checkedAt}` : ""})`;
|
|
57
|
+
}
|
|
58
|
+
function compactCandidate(row) {
|
|
59
|
+
const domains = Array.isArray(row.domains) ? row.domains : [];
|
|
60
|
+
const namespaces = Array.isArray(row.namespaces) ? row.namespaces : [];
|
|
61
|
+
const hardBlockers = Array.isArray(row.score?.hardBlockers) ? row.score.hardBlockers : [];
|
|
62
|
+
const namespaceCollisions = namespaces
|
|
63
|
+
.filter((namespace) => namespace.status === "taken" || namespace.status === "conflict")
|
|
64
|
+
.slice(0, 5)
|
|
65
|
+
.map((namespace) => `${namespace.namespace || namespace.label}: ${namespace.status}`);
|
|
66
|
+
const firstDomain = domains[0];
|
|
67
|
+
const verifyUrl = firstDomain?.manualUrl
|
|
68
|
+
|| (firstDomain ? `https://porkbun.com/checkout/search?q=${encodeURIComponent(firstDomain.domain.toLowerCase())}` : undefined);
|
|
69
|
+
return {
|
|
70
|
+
name: row.name,
|
|
71
|
+
...(typeof row.score?.overall === "number" ? { score: Math.round(row.score.overall) } : {}),
|
|
72
|
+
domains: domains.map(compactDomainLine),
|
|
73
|
+
...(hardBlockers.length ? { hardBlockers } : {}),
|
|
74
|
+
...(namespaceCollisions.length ? { namespaceCollisions } : {}),
|
|
75
|
+
...(verifyUrl ? { verifyUrl } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function normalizeTlds(values) {
|
|
79
|
+
return [...new Set(values.map((value) => value.trim().toLowerCase().replace(/^\./, "")).filter(Boolean))].slice(0, 5);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build a fully wired NamingSignal MCP server. The stdio entry file and the
|
|
83
|
+
* remote Streamable HTTP endpoint both call this factory so the tool surface,
|
|
84
|
+
* compact output contract, progress notifications, and idempotency behavior
|
|
85
|
+
* stay identical across transports.
|
|
86
|
+
*/
|
|
87
|
+
function createNamingSignalMcpServer(options = {}) {
|
|
88
|
+
const apiBase = (options.baseUrl?.trim() || exports.DEFAULT_API_URL).replace(/\/+$/, "");
|
|
89
|
+
const apiKey = options.apiKey?.trim() || undefined;
|
|
90
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
91
|
+
// One salt per server: an immediate retry of the same logical tool call
|
|
92
|
+
// re-derives the same Idempotency-Key and replays instead of re-charging.
|
|
93
|
+
const sessionSalt = options.idempotencySalt || (0, node_crypto_1.randomUUID)();
|
|
94
|
+
function idempotencyKey(path, body) {
|
|
95
|
+
return `mcp_${(0, node_crypto_1.createHash)("sha256").update(`${sessionSalt}\n${path}\n${stableStringify(body)}`).digest("hex").slice(0, 32)}`;
|
|
96
|
+
}
|
|
97
|
+
function networkErrorMessage(path, cause) {
|
|
98
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
99
|
+
const keyHint = apiKey
|
|
100
|
+
? ""
|
|
101
|
+
: " NAMING_SIGNAL_API_KEY is not set; hosted calls require a key created at https://namingsignal.com/developers.";
|
|
102
|
+
return `Could not reach ${apiBase}${path} (${detail}). Check NAMING_SIGNAL_API_URL if you meant a different server (for local development set NAMING_SIGNAL_API_URL=http://localhost:3000).${keyHint}`;
|
|
103
|
+
}
|
|
104
|
+
async function post(path, body, sprintId = requestId("mcp_sprint")) {
|
|
105
|
+
let response;
|
|
106
|
+
try {
|
|
107
|
+
response = await fetchImpl(`${apiBase}${path}`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
"Idempotency-Key": idempotencyKey(path, body),
|
|
112
|
+
"X-Name-Ledger-Sprint": sprintId,
|
|
113
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(body),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
throw new Error(networkErrorMessage(path, cause));
|
|
120
|
+
}
|
|
121
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
122
|
+
if (!contentType.includes("application/json")) {
|
|
123
|
+
throw new Error(`${path} returned HTTP ${response.status} ${response.statusText || ""} from ${apiBase} with a non-JSON response (${contentType || "no content type"}). ` +
|
|
124
|
+
`If this is a gateway error, retry; otherwise check NAMING_SIGNAL_API_URL.`);
|
|
125
|
+
}
|
|
126
|
+
const payload = await response.json();
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const hint = response.status === 401
|
|
129
|
+
? " Set NAMING_SIGNAL_API_KEY to a key created at /developers."
|
|
130
|
+
: "";
|
|
131
|
+
throw new Error(`${payload.error || payload.message || `${path} failed with HTTP ${response.status}`}${hint}`);
|
|
132
|
+
}
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
const server = new mcp_js_1.McpServer({
|
|
136
|
+
name: "naming-signal",
|
|
137
|
+
version: "0.2.0",
|
|
138
|
+
}, {
|
|
139
|
+
instructions: `${agent_guidance_1.AGENT_WORKFLOW_SUMMARY} Recommended MCP sequence: compile_project_brief when context quality is uncertain; review or correct the brief in conversation; create_naming_sprint with the corrected raw context and includeLedger=true when the complete checked field must be retained; check_names with namespaces enabled for a bounded survivor set; analyze_candidates for one to three finalists; then get_purchase_links for manual registrar verification. create_naming_sprint recompiles context, and runId is not a durable retrieval handle. Tool output is compact by default; pass verbose=true only when full evidence objects are required. Preserve strongestRegisteredOrBlocked and strongestNeedsReview instead of hiding trade-offs.`,
|
|
140
|
+
});
|
|
141
|
+
const readOnlyAnnotations = {
|
|
142
|
+
readOnlyHint: true,
|
|
143
|
+
destructiveHint: false,
|
|
144
|
+
idempotentHint: true,
|
|
145
|
+
openWorldHint: true,
|
|
146
|
+
};
|
|
147
|
+
server.registerTool("compile_project_brief", {
|
|
148
|
+
title: "Compile an editable naming brief",
|
|
149
|
+
description: "Turn a sentence, product prompt, README, or agent summary into a bounded, editable project brief. Missing fields remain explicit rather than invented.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
context: v4_1.z.string().min(3).max(250_000).describe("Product context, prompt, or README text"),
|
|
152
|
+
source: v4_1.z.string().min(1).max(200).default("MCP context").describe("Human-readable provenance label"),
|
|
153
|
+
},
|
|
154
|
+
annotations: readOnlyAnnotations,
|
|
155
|
+
}, async ({ context, source }) => {
|
|
156
|
+
const payload = await post("/api/v1/brief", { text: context, source });
|
|
157
|
+
return toolResult({ brief: payload.brief, preview: payload.preview });
|
|
158
|
+
});
|
|
159
|
+
server.registerTool("create_naming_sprint", {
|
|
160
|
+
title: "Create and check a naming sprint",
|
|
161
|
+
description: "Generate and check a naming sprint across four directions. Use any count from 10 to 200; 100–200 for an initial sprint or 40 for a feedback round. Pass prior names as existingNames so an iteration never repeats the retained ledger.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
context: v4_1.z.string().min(3).max(250_000).describe("Product description, prompt, or README text"),
|
|
164
|
+
tlds: v4_1.z.array(v4_1.z.string().regex(/^\.?[a-z0-9-]{2,24}$/i)).min(1).max(5).default(DEFAULT_TLDS),
|
|
165
|
+
count: v4_1.z.number().int().min(10).max(200).default(100),
|
|
166
|
+
availabilityRule: v4_1.z.enum(["any_selected", "all_selected"]).default("any_selected"),
|
|
167
|
+
top: v4_1.z.number().int().min(1).max(50).default(20).describe("Number of ranked survivors returned inline"),
|
|
168
|
+
includeLedger: v4_1.z.boolean().default(false).describe("Return the complete checked candidate ledger in addition to the bounded shortlist"),
|
|
169
|
+
verbose: v4_1.z.boolean().default(false).describe("Return full evidence objects instead of the compact per-name shape"),
|
|
170
|
+
feedback: v4_1.z.string().min(3).max(2_000).optional().describe("Founder feedback for an iterative round"),
|
|
171
|
+
likedNames: v4_1.z.array(v4_1.z.string().min(2).max(32)).max(30).default([]).describe("Examples whose qualities should guide the next round"),
|
|
172
|
+
dislikedNames: v4_1.z.array(v4_1.z.string().min(2).max(32)).max(30).default([]).describe("Examples or patterns the next round should avoid"),
|
|
173
|
+
existingNames: v4_1.z.array(v4_1.z.string().min(2).max(32)).max(500).default([]).describe("Names already generated in this project; all are excluded from the new round"),
|
|
174
|
+
},
|
|
175
|
+
annotations: readOnlyAnnotations,
|
|
176
|
+
}, async ({ context, tlds: rawTlds, count, availabilityRule, top, includeLedger, verbose, feedback, likedNames, dislikedNames, existingNames }, extra) => {
|
|
177
|
+
const progressToken = extra?._meta?.progressToken;
|
|
178
|
+
const generationBatches = Math.ceil(count / 50);
|
|
179
|
+
let progress = 0;
|
|
180
|
+
let total = 1 + generationBatches + 2; // brief + generation + checks + done; refined once check batching is known.
|
|
181
|
+
const notifyProgress = async (message) => {
|
|
182
|
+
if (progressToken === undefined || progressToken === null)
|
|
183
|
+
return;
|
|
184
|
+
progress += 1;
|
|
185
|
+
try {
|
|
186
|
+
await extra.sendNotification({
|
|
187
|
+
method: "notifications/progress",
|
|
188
|
+
params: { progressToken, progress, total: Math.max(total, progress), message },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Progress is best-effort; a transport hiccup must not fail the sprint.
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const sprintId = requestId("mcp_sprint");
|
|
196
|
+
const tlds = normalizeTlds(rawTlds);
|
|
197
|
+
const briefPayload = await post("/api/v1/brief", { text: context, source: "MCP naming sprint" }, sprintId);
|
|
198
|
+
if (!briefPayload.brief)
|
|
199
|
+
throw new Error("The brief route returned no project brief.");
|
|
200
|
+
const brief = briefPayload.brief;
|
|
201
|
+
await notifyProgress("Brief compiled");
|
|
202
|
+
const candidates = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
const usage = {};
|
|
205
|
+
const fallbackReasons = new Set();
|
|
206
|
+
let attempts = 0;
|
|
207
|
+
while (candidates.length < count && attempts < generationBatches + 2) {
|
|
208
|
+
// The generate route accepts 4–50 names per call; clamp small remainders up to 4.
|
|
209
|
+
const requested = Math.min(50, Math.max(4, count - candidates.length));
|
|
210
|
+
const payload = await post("/api/v1/generate", {
|
|
211
|
+
brief: {
|
|
212
|
+
...brief,
|
|
213
|
+
domainRequirements: [
|
|
214
|
+
`${availabilityRule === "all_selected" ? "Every" : "At least one"} selected domain must be likely unregistered`,
|
|
215
|
+
...tlds.map((tld) => `.${tld}`),
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
count: requested,
|
|
219
|
+
tlds,
|
|
220
|
+
checkPrimaryDomain: false,
|
|
221
|
+
excludedNames: [...existingNames, ...candidates.map((candidate) => candidate.name)].slice(-500),
|
|
222
|
+
...(feedback ? { iteration: { feedback, likedNames, dislikedNames } } : {}),
|
|
223
|
+
}, sprintId);
|
|
224
|
+
mergeUsage(usage, payload.usage);
|
|
225
|
+
if (payload.fallback)
|
|
226
|
+
fallbackReasons.add(payload.fallbackReason || "deterministic_fallback");
|
|
227
|
+
let added = 0;
|
|
228
|
+
for (const candidate of payload.candidates ?? []) {
|
|
229
|
+
const key = (0, index_1.normalizeName)(candidate.name).replace(/\s+/g, "");
|
|
230
|
+
if (!key || seen.has(key))
|
|
231
|
+
continue;
|
|
232
|
+
seen.add(key);
|
|
233
|
+
candidates.push(candidate);
|
|
234
|
+
added += 1;
|
|
235
|
+
}
|
|
236
|
+
attempts += 1;
|
|
237
|
+
await notifyProgress(`Generated ${Math.min(candidates.length, count)}/${count} candidates (batch ${attempts})`);
|
|
238
|
+
if (!added)
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (!candidates.length)
|
|
242
|
+
throw new Error("The naming sprint produced no distinct candidates.");
|
|
243
|
+
if (candidates.length > count)
|
|
244
|
+
candidates.length = count;
|
|
245
|
+
const checked = new Map(candidates.map((candidate) => [(0, index_1.normalizeName)(candidate.name).replace(/\s+/g, ""), candidate]));
|
|
246
|
+
const namesPerBatch = Math.max(1, Math.floor(100 / tlds.length));
|
|
247
|
+
const checkBatches = (0, batch_1.chunkItems)(candidates.map((candidate) => candidate.name), namesPerBatch);
|
|
248
|
+
const checkGroups = (0, batch_1.chunkItems)(checkBatches, 3);
|
|
249
|
+
total = progress + checkGroups.length + 1;
|
|
250
|
+
let checkedCount = 0;
|
|
251
|
+
for (const batchGroup of checkGroups) {
|
|
252
|
+
const payloads = await Promise.all(batchGroup.map((names) => post("/api/v1/check", { names, tlds, context: brief, includeNamespaces: false }, sprintId)));
|
|
253
|
+
for (const payload of payloads) {
|
|
254
|
+
mergeUsage(usage, payload.usage);
|
|
255
|
+
for (const result of payload.results ?? []) {
|
|
256
|
+
const key = (0, index_1.normalizeName)(result.name).replace(/\s+/g, "");
|
|
257
|
+
const original = checked.get(key);
|
|
258
|
+
if (original)
|
|
259
|
+
checked.set(key, { ...original, ...result });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
checkedCount += batchGroup.reduce((sum, names) => sum + names.length, 0);
|
|
263
|
+
await notifyProgress(`Checked ${checkedCount}/${candidates.length} names`);
|
|
264
|
+
}
|
|
265
|
+
const completed = [...checked.values()];
|
|
266
|
+
const rule = availabilityRule;
|
|
267
|
+
const registrySurvivors = (0, batch_1.sortCandidates)(completed.filter((candidate) => (0, batch_1.classifyCandidate)(candidate, tlds, rule) === "available"));
|
|
268
|
+
const registeredOrBlocked = completed.filter((candidate) => (0, batch_1.classifyCandidate)(candidate, tlds, rule) === "taken");
|
|
269
|
+
const needsReview = completed.filter((candidate) => (0, batch_1.classifyCandidate)(candidate, tlds, rule) === "review");
|
|
270
|
+
await notifyProgress("Sprint complete");
|
|
271
|
+
const shared = {
|
|
272
|
+
runId: sprintId,
|
|
273
|
+
constraints: { tlds, count, availabilityRule },
|
|
274
|
+
counts: {
|
|
275
|
+
generated: completed.length,
|
|
276
|
+
registrySurvivors: registrySurvivors.length,
|
|
277
|
+
registeredOrBlocked: registeredOrBlocked.length,
|
|
278
|
+
needsReview: needsReview.length,
|
|
279
|
+
},
|
|
280
|
+
fallback: fallbackReasons.size > 0,
|
|
281
|
+
fallbackReasons: [...fallbackReasons],
|
|
282
|
+
usage,
|
|
283
|
+
disclaimer: "Registry survivors are likely unregistered signals, not guaranteed registrability. Recheck at a registrar before purchase. Preliminary naming research is not legal clearance.",
|
|
284
|
+
};
|
|
285
|
+
if (!verbose) {
|
|
286
|
+
return toolResult({
|
|
287
|
+
...shared,
|
|
288
|
+
candidates: registrySurvivors.slice(0, top).map((candidate) => ({
|
|
289
|
+
...compactCandidate(candidate),
|
|
290
|
+
vein: candidate.vein,
|
|
291
|
+
})),
|
|
292
|
+
strongestRegisteredOrBlocked: (0, batch_1.sortCandidates)(registeredOrBlocked).slice(0, Math.min(5, top)).map(compactCandidate),
|
|
293
|
+
strongestNeedsReview: (0, batch_1.sortCandidates)(needsReview).slice(0, Math.min(5, top)).map(compactCandidate),
|
|
294
|
+
...(includeLedger ? { ledger: (0, batch_1.sortCandidates)(completed).map(compactCandidate) } : {}),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return toolResult({
|
|
298
|
+
...shared,
|
|
299
|
+
brief,
|
|
300
|
+
candidates: registrySurvivors.slice(0, top).map((candidate) => ({
|
|
301
|
+
name: candidate.name,
|
|
302
|
+
vein: candidate.vein,
|
|
303
|
+
rationale: candidate.rationale,
|
|
304
|
+
overallScore: candidate.score?.overall,
|
|
305
|
+
bestDomain: (0, batch_1.bestQualifyingDomain)(candidate, tlds, rule),
|
|
306
|
+
domains: candidate.domains,
|
|
307
|
+
})),
|
|
308
|
+
strongestRegisteredOrBlocked: (0, batch_1.sortCandidates)(registeredOrBlocked).slice(0, Math.min(10, top)).map((candidate) => ({
|
|
309
|
+
name: candidate.name,
|
|
310
|
+
vein: candidate.vein,
|
|
311
|
+
rationale: candidate.rationale,
|
|
312
|
+
overallScore: candidate.score?.overall,
|
|
313
|
+
domains: candidate.domains,
|
|
314
|
+
})),
|
|
315
|
+
strongestNeedsReview: (0, batch_1.sortCandidates)(needsReview).slice(0, Math.min(10, top)).map((candidate) => ({
|
|
316
|
+
name: candidate.name,
|
|
317
|
+
vein: candidate.vein,
|
|
318
|
+
rationale: candidate.rationale,
|
|
319
|
+
overallScore: candidate.score?.overall,
|
|
320
|
+
domains: candidate.domains,
|
|
321
|
+
})),
|
|
322
|
+
...(includeLedger ? {
|
|
323
|
+
ledger: (0, batch_1.sortCandidates)(completed).map((candidate) => ({
|
|
324
|
+
name: candidate.name,
|
|
325
|
+
vein: candidate.vein,
|
|
326
|
+
rationale: candidate.rationale,
|
|
327
|
+
overallScore: candidate.score?.overall,
|
|
328
|
+
bestDomain: (0, batch_1.bestQualifyingDomain)(candidate, tlds, rule),
|
|
329
|
+
domains: candidate.domains,
|
|
330
|
+
})),
|
|
331
|
+
} : {}),
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
server.registerTool("check_names", {
|
|
335
|
+
title: "Check names and selected domain endings",
|
|
336
|
+
description: "Check exact domain registry evidence and optional developer namespaces for one or more names. Unknown, timeout, unsupported, or conflicting evidence never becomes available.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
names: v4_1.z.array(v4_1.z.string().min(2).max(64)).min(1).max(100),
|
|
339
|
+
tlds: v4_1.z.array(v4_1.z.string().regex(/^\.?[a-z0-9-]{2,24}$/i)).min(1).max(5).default(DEFAULT_TLDS),
|
|
340
|
+
includeNamespaces: v4_1.z.boolean().default(true),
|
|
341
|
+
verbose: v4_1.z.boolean().default(false).describe("Return full evidence objects instead of the compact per-name shape"),
|
|
342
|
+
context: v4_1.z.string().max(10_000).optional().describe("Optional product context for transparent scoring"),
|
|
343
|
+
},
|
|
344
|
+
annotations: readOnlyAnnotations,
|
|
345
|
+
}, async ({ names, tlds: rawTlds, includeNamespaces, verbose, context }) => {
|
|
346
|
+
const tlds = normalizeTlds(rawTlds);
|
|
347
|
+
const sprintId = requestId("mcp_check");
|
|
348
|
+
const domainBatchSize = Math.max(1, Math.floor(100 / tlds.length));
|
|
349
|
+
const batchSize = includeNamespaces ? Math.min(domainBatchSize, 20) : domainBatchSize;
|
|
350
|
+
const results = [];
|
|
351
|
+
const usage = {};
|
|
352
|
+
const boundedDegradation = [];
|
|
353
|
+
for (const batch of (0, batch_1.chunkItems)(names, batchSize)) {
|
|
354
|
+
const payload = await post("/api/v1/check", { names: batch, tlds, includeNamespaces, context: context ? { product: context } : undefined }, sprintId);
|
|
355
|
+
results.push(...(payload.results ?? []));
|
|
356
|
+
mergeUsage(usage, payload.usage);
|
|
357
|
+
if (payload.boundedDegradation)
|
|
358
|
+
boundedDegradation.push(payload.boundedDegradation);
|
|
359
|
+
}
|
|
360
|
+
const interpretation = "RDAP not-found is likely unregistered, not a purchase guarantee.";
|
|
361
|
+
if (!verbose) {
|
|
362
|
+
return toolResult({
|
|
363
|
+
results: results.map(compactCandidate),
|
|
364
|
+
usage,
|
|
365
|
+
...(boundedDegradation.length ? { boundedDegradation } : {}),
|
|
366
|
+
interpretation,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return toolResult({ results, usage, boundedDegradation, interpretation });
|
|
370
|
+
});
|
|
371
|
+
server.registerTool("analyze_candidates", {
|
|
372
|
+
title: "Analyze one to three finalists",
|
|
373
|
+
description: "Run deeper source-backed namesake, namespace, domain, and preliminary trademark-risk research for one to three finalists. Returns unknowns and manual checks explicitly.",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
names: v4_1.z.array(v4_1.z.string().min(2).max(64)).min(1).max(3),
|
|
376
|
+
context: v4_1.z.string().min(3).max(100_000),
|
|
377
|
+
tlds: v4_1.z.array(v4_1.z.string().regex(/^\.?[a-z0-9-]{2,24}$/i)).min(1).max(5).default(DEFAULT_TLDS),
|
|
378
|
+
verbose: v4_1.z.boolean().default(false).describe("Return full research evidence instead of the compact per-name shape"),
|
|
379
|
+
},
|
|
380
|
+
annotations: readOnlyAnnotations,
|
|
381
|
+
}, async ({ names, context, tlds: rawTlds, verbose }) => {
|
|
382
|
+
const sprintId = requestId("mcp_research");
|
|
383
|
+
const tlds = normalizeTlds(rawTlds);
|
|
384
|
+
const briefPayload = await post("/api/v1/brief", { text: context, source: "MCP finalist context" }, sprintId);
|
|
385
|
+
const payload = await post("/api/v1/research", { names, brief: briefPayload.brief, tlds }, sprintId);
|
|
386
|
+
const disclaimer = "Preliminary research only; not legal advice or legal clearance.";
|
|
387
|
+
if (!verbose) {
|
|
388
|
+
const rows = (payload.results ?? []);
|
|
389
|
+
return toolResult({
|
|
390
|
+
results: rows.map((row) => ({
|
|
391
|
+
...compactCandidate(row),
|
|
392
|
+
...(typeof row.strongestReasonFor === "string" ? { strongestReasonFor: row.strongestReasonFor } : {}),
|
|
393
|
+
...(typeof row.strongestReasonAgainst === "string" ? { strongestReasonAgainst: row.strongestReasonAgainst } : {}),
|
|
394
|
+
})),
|
|
395
|
+
usage: payload.usage,
|
|
396
|
+
disclaimer,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return toolResult({ results: payload.results, usage: payload.usage, disclaimer });
|
|
400
|
+
});
|
|
401
|
+
server.registerTool("get_purchase_links", {
|
|
402
|
+
title: "Get registrar verification links",
|
|
403
|
+
description: "Create exact-domain Porkbun search links for a fresh manual availability and price check. This never purchases, reserves, or claims a domain is available.",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
domains: v4_1.z.array(v4_1.z.string().regex(/^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i)).min(1).max(20),
|
|
406
|
+
},
|
|
407
|
+
annotations: { ...readOnlyAnnotations, openWorldHint: false },
|
|
408
|
+
}, async ({ domains }) => toolResult({
|
|
409
|
+
links: domains.map((domain) => ({
|
|
410
|
+
domain: domain.toLowerCase(),
|
|
411
|
+
registrar: "Porkbun",
|
|
412
|
+
url: `https://porkbun.com/checkout/search?q=${encodeURIComponent(domain.toLowerCase())}`,
|
|
413
|
+
evidenceState: "manual",
|
|
414
|
+
})),
|
|
415
|
+
disclosure: "Availability and prices can change. Review first-year and renewal prices, premium status, minimum term, fees, taxes, and final registrability at checkout. NamingSignal does not reserve or purchase domains.",
|
|
416
|
+
}));
|
|
417
|
+
return server;
|
|
418
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@namingsignal/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stdio MCP server for NamingSignal: evidence-first product naming with exact domain registry checks, namespace checks, and finalist research.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://namingsignal.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/moekoelueker/namingsignal.git",
|
|
10
|
+
"directory": "packages/mcp"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/moekoelueker/namingsignal/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"naming",
|
|
17
|
+
"domain",
|
|
18
|
+
"rdap",
|
|
19
|
+
"claude",
|
|
20
|
+
"codex",
|
|
21
|
+
"agent"
|
|
22
|
+
],
|
|
23
|
+
"mcpName": "com.namingsignal/naming-signal",
|
|
24
|
+
"bin": {
|
|
25
|
+
"namingsignal-mcp": "dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"main": "dist/server-core.js",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"prepack": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.19.19",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
}
|
|
48
|
+
}
|