@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zena Labs LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @namingsignal/mcp
|
|
2
|
+
|
|
3
|
+
Stdio [Model Context Protocol](https://modelcontextprotocol.io) server for [NamingSignal](https://namingsignal.com): evidence-first product naming with exact domain registry checks, developer namespace checks, and source-backed finalist research. All five tools are read-only and are metered through the NamingSignal account attached to your API key.
|
|
4
|
+
|
|
5
|
+
Create a free API key at https://namingsignal.com/developers/keys.
|
|
6
|
+
|
|
7
|
+
## Fastest option: remote MCP, no install
|
|
8
|
+
|
|
9
|
+
NamingSignal also serves the identical tool surface over Streamable HTTP at `https://namingsignal.com/mcp`. If your client supports remote MCP servers, you do not need this package at all.
|
|
10
|
+
|
|
11
|
+
Claude Code:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add --transport http naming-signal https://namingsignal.com/mcp \
|
|
15
|
+
--header "Authorization: Bearer YOUR_NAMINGSIGNAL_KEY"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Codex `~/.codex/config.toml`:
|
|
19
|
+
|
|
20
|
+
```toml
|
|
21
|
+
experimental_use_rmcp_client = true
|
|
22
|
+
|
|
23
|
+
[mcp_servers.naming_signal]
|
|
24
|
+
url = "https://namingsignal.com/mcp"
|
|
25
|
+
bearer_token_env_var = "NAMING_SIGNAL_API_KEY"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Stdio setup
|
|
29
|
+
|
|
30
|
+
The stdio server runs locally and calls the hosted REST API (default `https://namingsignal.com`).
|
|
31
|
+
|
|
32
|
+
Claude Code:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add naming-signal \
|
|
36
|
+
--env NAMING_SIGNAL_API_KEY=YOUR_NAMINGSIGNAL_KEY \
|
|
37
|
+
-- npx -y @namingsignal/mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Codex `~/.codex/config.toml`:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
[mcp_servers.naming_signal]
|
|
44
|
+
command = "npx"
|
|
45
|
+
args = ["-y", "@namingsignal/mcp"]
|
|
46
|
+
env = { NAMING_SIGNAL_API_KEY = "YOUR_NAMINGSIGNAL_KEY" }
|
|
47
|
+
tool_timeout_sec = 360
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Large sprints (150 to 200 names across several TLDs) can take minutes in one synchronous tool call; raise your client's tool timeout (Codex `tool_timeout_sec = 360`) before running one.
|
|
51
|
+
|
|
52
|
+
### Environment variables
|
|
53
|
+
|
|
54
|
+
| Variable | Required | Meaning |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `NAMING_SIGNAL_API_KEY` | Yes, for hosted tool calls | User API key created at https://namingsignal.com/developers/keys. Sent as the Authorization Bearer header, never logged. |
|
|
57
|
+
| `NAMING_SIGNAL_API_URL` | No | API origin. Defaults to `https://namingsignal.com`; set `http://localhost:3000` when running the API locally. |
|
|
58
|
+
|
|
59
|
+
## The five tools
|
|
60
|
+
|
|
61
|
+
- `compile_project_brief`: turn a sentence, prompt, or README into a bounded, editable naming brief; missing fields stay explicit instead of being invented.
|
|
62
|
+
- `create_naming_sprint`: generate and check 10 to 200 names across up to five TLDs, with feedback rounds (`feedback`, `likedNames`, `dislikedNames`, `existingNames`) and an optional full ledger (`includeLedger: true`).
|
|
63
|
+
- `check_names`: exact domain registry evidence and optional npm/PyPI/GitHub/Apple namespace checks for up to 100 names.
|
|
64
|
+
- `analyze_candidates`: deeper source-backed research for one to three finalists, with explicit unknowns and manual checks.
|
|
65
|
+
- `get_purchase_links`: exact-domain registrar search links for manual verification; it never purchases or reserves anything.
|
|
66
|
+
|
|
67
|
+
Tool output is compact by default; pass `verbose: true` only when full evidence objects are required.
|
|
68
|
+
|
|
69
|
+
## Evidence honesty
|
|
70
|
+
|
|
71
|
+
Registry RDAP not-found means likely unregistered at check time, not guaranteed registrability, registrar price, or trademark clearance. Timeout, rate limit, unsupported TLD, failed calibration, and missing research remain unknown and are never converted into available. Recheck every finalist at a registrar before purchase; naming research is not legal clearance.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT, Copyright (c) 2026 Zena Labs LLC.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_WORKFLOW_SUMMARY = void 0;
|
|
4
|
+
// Vendored from the NamingSignal product source so the published stdio MCP
|
|
5
|
+
// package stays self-contained. Keep this text in sync with the hosted agent
|
|
6
|
+
// guide at https://namingsignal.com/agent-guide.txt.
|
|
7
|
+
exports.AGENT_WORKFLOW_SUMMARY = `Use NamingSignal as an evidence-first workflow, not a one-shot name generator. Compile and inspect the brief before generation; correct material product, audience, pain, outcome, mechanism, channel, tone, future-scope, domain, and avoid constraints. Generate a broad field in distinct veins, preserve the ledger, bulk-check domains, check exact namespaces on survivors, and research only one to three finalists. When a founder gives feedback, preserve the approved brief, send liked and disliked examples as preference signals, exclude every prior name, and merge the checked new round into the same project ledger. Treat registry RDAP not-found only as likely unregistered. Timeout, rate limit, unsupported, failed calibration, ambiguity, and missing research remain unknown. Keep hard blockers and active same-category collisions separate from scores. Label deterministic fallback, overall score, and AI editorial rank accurately. Preserve sources, timestamps, evidence state, fallback/usage metadata, feedback rounds, rejections, and manual actions. Require current registrar verification, founder resonance, spoken-name testing when relevant, cultural review, and appropriate legal clearance. NamingSignal never purchases, reserves, files, contacts, or claims legal clearance.`;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.chunkItems = chunkItems;
|
|
4
|
+
exports.classifyCandidate = classifyCandidate;
|
|
5
|
+
exports.bestQualifyingDomain = bestQualifyingDomain;
|
|
6
|
+
exports.sortCandidates = sortCandidates;
|
|
7
|
+
const TAKEN_DOMAIN_STATES = new Set([
|
|
8
|
+
"registered",
|
|
9
|
+
"premium",
|
|
10
|
+
"parked",
|
|
11
|
+
"active",
|
|
12
|
+
]);
|
|
13
|
+
function normalizeTld(value) {
|
|
14
|
+
const normalized = value.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
15
|
+
return /^[a-z0-9-]+$/.test(normalized) ? normalized : "";
|
|
16
|
+
}
|
|
17
|
+
function selectedTlds(values) {
|
|
18
|
+
const result = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const value of values) {
|
|
21
|
+
const tld = normalizeTld(value);
|
|
22
|
+
if (!tld || seen.has(tld))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(tld);
|
|
25
|
+
result.push(tld);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function domainTld(domain) {
|
|
30
|
+
const labels = domain.trim().toLowerCase().replace(/\.$/, "").split(".");
|
|
31
|
+
return labels.length > 1 ? labels.at(-1) ?? "" : "";
|
|
32
|
+
}
|
|
33
|
+
function stateForTld(domains, tld) {
|
|
34
|
+
const states = new Set();
|
|
35
|
+
for (const domain of domains) {
|
|
36
|
+
if (domainTld(domain.domain) !== tld)
|
|
37
|
+
continue;
|
|
38
|
+
if (domain.status === "likely_available")
|
|
39
|
+
states.add("available");
|
|
40
|
+
else if (TAKEN_DOMAIN_STATES.has(domain.status))
|
|
41
|
+
states.add("taken");
|
|
42
|
+
else
|
|
43
|
+
states.add("review");
|
|
44
|
+
}
|
|
45
|
+
// Missing evidence and conflicting duplicate evidence both require review.
|
|
46
|
+
if (states.size !== 1)
|
|
47
|
+
return "review";
|
|
48
|
+
return states.values().next().value ?? "review";
|
|
49
|
+
}
|
|
50
|
+
function chunkItems(items, chunkSize) {
|
|
51
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1) {
|
|
52
|
+
throw new RangeError("Chunk size must be a positive safe integer.");
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for (let index = 0; index < items.length; index += chunkSize) {
|
|
56
|
+
chunks.push(items.slice(index, index + chunkSize));
|
|
57
|
+
}
|
|
58
|
+
return chunks;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Classify only against the selected TLDs. Unknown, unsupported, missing, or
|
|
62
|
+
* conflicting evidence never becomes available or taken by implication.
|
|
63
|
+
*/
|
|
64
|
+
function classifyCandidate(candidate, tlds, rule) {
|
|
65
|
+
const normalizedTlds = selectedTlds(tlds);
|
|
66
|
+
if (!normalizedTlds.length)
|
|
67
|
+
return "review";
|
|
68
|
+
const states = normalizedTlds.map((tld) => stateForTld(candidate.domains ?? [], tld));
|
|
69
|
+
if (rule === "any_selected") {
|
|
70
|
+
if (states.includes("available"))
|
|
71
|
+
return "available";
|
|
72
|
+
return states.every((state) => state === "taken") ? "taken" : "review";
|
|
73
|
+
}
|
|
74
|
+
if (rule === "all_selected") {
|
|
75
|
+
if (states.every((state) => state === "available"))
|
|
76
|
+
return "available";
|
|
77
|
+
return states.includes("taken") ? "taken" : "review";
|
|
78
|
+
}
|
|
79
|
+
throw new TypeError(`Unsupported availability rule: ${String(rule)}`);
|
|
80
|
+
}
|
|
81
|
+
/** Return the highest-priority selected domain that satisfies the rule. */
|
|
82
|
+
function bestQualifyingDomain(candidate, tlds, rule = "any_selected") {
|
|
83
|
+
if (classifyCandidate(candidate, tlds, rule) !== "available")
|
|
84
|
+
return undefined;
|
|
85
|
+
const domains = candidate.domains ?? [];
|
|
86
|
+
for (const tld of selectedTlds(tlds)) {
|
|
87
|
+
const result = domains.find((domain) => domainTld(domain.domain) === tld && domain.status === "likely_available");
|
|
88
|
+
if (result)
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function finiteScore(value) {
|
|
94
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
function scoreForKey(score, key) {
|
|
97
|
+
if (!score)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (key === "overall")
|
|
100
|
+
return finiteScore(score.overall);
|
|
101
|
+
return finiteScore(score.dimensions.find((dimension) => dimension.key === key)?.score);
|
|
102
|
+
}
|
|
103
|
+
function compareScoresDescending(left, right) {
|
|
104
|
+
if (left === undefined && right === undefined)
|
|
105
|
+
return 0;
|
|
106
|
+
if (left === undefined)
|
|
107
|
+
return 1;
|
|
108
|
+
if (right === undefined)
|
|
109
|
+
return -1;
|
|
110
|
+
return right - left;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Return a new deterministic ranking. The selected score descends first,
|
|
114
|
+
* followed by overall score, display name, and original order as the final
|
|
115
|
+
* stable tie-break.
|
|
116
|
+
*/
|
|
117
|
+
function sortCandidates(candidates, key = "overall") {
|
|
118
|
+
return candidates
|
|
119
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
120
|
+
.sort((left, right) => {
|
|
121
|
+
const primary = compareScoresDescending(scoreForKey(left.candidate.score, key), scoreForKey(right.candidate.score, key));
|
|
122
|
+
if (primary)
|
|
123
|
+
return primary;
|
|
124
|
+
if (key !== "overall") {
|
|
125
|
+
const overall = compareScoresDescending(finiteScore(left.candidate.score?.overall), finiteScore(right.candidate.score?.overall));
|
|
126
|
+
if (overall)
|
|
127
|
+
return overall;
|
|
128
|
+
}
|
|
129
|
+
const insensitiveName = left.candidate.name.localeCompare(right.candidate.name, "en", {
|
|
130
|
+
numeric: true,
|
|
131
|
+
sensitivity: "base",
|
|
132
|
+
});
|
|
133
|
+
if (insensitiveName)
|
|
134
|
+
return insensitiveName;
|
|
135
|
+
const exactName = left.candidate.name.localeCompare(right.candidate.name, "en", {
|
|
136
|
+
numeric: true,
|
|
137
|
+
sensitivity: "variant",
|
|
138
|
+
});
|
|
139
|
+
return exactName || left.index - right.index;
|
|
140
|
+
})
|
|
141
|
+
.map(({ candidate }) => candidate);
|
|
142
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.explicitAvoidTerms = explicitAvoidTerms;
|
|
4
|
+
exports.candidateAvoidViolations = candidateAvoidViolations;
|
|
5
|
+
exports.candidateBriefConflicts = candidateBriefConflicts;
|
|
6
|
+
const normalize_1 = require("./normalize");
|
|
7
|
+
function explicitAvoidTerms(brief) {
|
|
8
|
+
if (!brief)
|
|
9
|
+
return [];
|
|
10
|
+
const terms = new Set();
|
|
11
|
+
for (const entry of brief.avoid) {
|
|
12
|
+
const normalized = (0, normalize_1.normalizeName)(entry);
|
|
13
|
+
const words = normalized.split(" ").filter(Boolean);
|
|
14
|
+
if (words.length <= 2 && normalized.length <= 28)
|
|
15
|
+
terms.add((0, normalize_1.comparisonKey)(normalized));
|
|
16
|
+
const examples = entry.match(/(?:such as|including|brands?\s*:?)\s+(.+)$/i)?.[1];
|
|
17
|
+
if (examples) {
|
|
18
|
+
for (const example of examples.split(/[,;/]|\band\b/i)) {
|
|
19
|
+
const key = (0, normalize_1.comparisonKey)(example);
|
|
20
|
+
if (key.length >= 3 && key.length <= 24)
|
|
21
|
+
terms.add(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...terms].filter((term) => term.length >= 3);
|
|
26
|
+
}
|
|
27
|
+
function candidateAvoidViolations(name, brief) {
|
|
28
|
+
const key = (0, normalize_1.comparisonKey)(name);
|
|
29
|
+
return explicitAvoidTerms(brief).filter((term) => key.includes(term));
|
|
30
|
+
}
|
|
31
|
+
function candidateBriefConflicts(name, brief) {
|
|
32
|
+
if (!brief)
|
|
33
|
+
return { blockers: [], warnings: [], penalty: 0 };
|
|
34
|
+
const key = (0, normalize_1.comparisonKey)(name);
|
|
35
|
+
const context = (0, normalize_1.normalizeName)([
|
|
36
|
+
brief.product,
|
|
37
|
+
brief.pain,
|
|
38
|
+
brief.outcome,
|
|
39
|
+
brief.mechanism,
|
|
40
|
+
brief.futureExpansion,
|
|
41
|
+
...brief.avoid,
|
|
42
|
+
].join(" "));
|
|
43
|
+
const blockers = [];
|
|
44
|
+
const warnings = [];
|
|
45
|
+
let penalty = 0;
|
|
46
|
+
const avoided = candidateAvoidViolations(name, brief);
|
|
47
|
+
if (avoided.length) {
|
|
48
|
+
blockers.push(`${name} contains an explicitly avoided term: ${avoided.join(", ")}.`);
|
|
49
|
+
}
|
|
50
|
+
if (/\b(?:audio|audible|voice|announces|calls every rep|podcast|music)\b/.test(context) && /(?:inaudible|voiceless|muted)/.test(key)) {
|
|
51
|
+
blockers.push(`${name} contradicts the brief's audible-guidance mechanism.`);
|
|
52
|
+
}
|
|
53
|
+
if (/\b(?:finish|completion|purpose|useful|guided|carries the workout)\b/.test(context) && /(?:aimless|purposeless)/.test(key)) {
|
|
54
|
+
blockers.push(`${name} contradicts the brief's purposeful-completion outcome.`);
|
|
55
|
+
}
|
|
56
|
+
if (/\b(?:avoid|not aggressive|gym bro|punishment|negative)\b/.test(context) && /(?:mindless|gainz|shred|beast|crush|punish)/.test(key)) {
|
|
57
|
+
blockers.push(`${name} uses the negative or gym-bro language the brief excludes.`);
|
|
58
|
+
}
|
|
59
|
+
if (/\b(?:relief|without managing|low decision|mental overhead|autopilot)\b/.test(context) && /(?:tethered|trapped|bound)/.test(key)) {
|
|
60
|
+
warnings.push(`${name} implies restriction while the brief promises relief from micromanagement.`);
|
|
61
|
+
penalty += 18;
|
|
62
|
+
}
|
|
63
|
+
if (/\b(?:workout|exercise|reps|sets)\b/.test(context) && !/\b(?:cycling|bike|bicycle|ride)\b/.test(context) && /(?:pedal|bicycle|cycling)/.test(key)) {
|
|
64
|
+
warnings.push(`${name} suggests cycling even though the brief describes a broader workout product.`);
|
|
65
|
+
penalty += 22;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
blockers: [...new Set(blockers)],
|
|
69
|
+
warnings: [...new Set(warnings)],
|
|
70
|
+
penalty: Math.min(35, penalty),
|
|
71
|
+
};
|
|
72
|
+
}
|