katman-mcp 0.1.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 +13 -0
- package/README.md +160 -0
- package/dist/cli.js +71 -0
- package/dist/context.js +43 -0
- package/dist/crawlers.js +86 -0
- package/dist/index.js +25 -0
- package/dist/license.js +448 -0
- package/dist/meta.js +28 -0
- package/dist/net.js +128 -0
- package/dist/server.js +17 -0
- package/dist/state.js +81 -0
- package/dist/tools/account/account.js +64 -0
- package/dist/tools/account/cli.js +21 -0
- package/dist/tools/account/index.js +48 -0
- package/dist/tools/account/sync.js +142 -0
- package/dist/tools/account/upsell.js +290 -0
- package/dist/tools/crawl/audit/analyze.js +719 -0
- package/dist/tools/crawl/audit/bots.js +114 -0
- package/dist/tools/crawl/audit/checks.js +126 -0
- package/dist/tools/crawl/audit/html.js +382 -0
- package/dist/tools/crawl/audit/index.js +364 -0
- package/dist/tools/crawl/audit/model.js +1 -0
- package/dist/tools/crawl/audit/report.js +199 -0
- package/dist/tools/crawl/audit/robots.js +199 -0
- package/dist/tools/crawl/audit/sitemap.js +147 -0
- package/dist/tools/crawl/cli.js +106 -0
- package/dist/tools/crawl/common/advice.js +61 -0
- package/dist/tools/crawl/common/findings.js +57 -0
- package/dist/tools/crawl/common/routes.js +80 -0
- package/dist/tools/crawl/common/text.js +89 -0
- package/dist/tools/crawl/common/urls.js +64 -0
- package/dist/tools/crawl/index.js +226 -0
- package/dist/tools/crawl/indexnow.js +271 -0
- package/dist/tools/crawl/keywords.js +178 -0
- package/dist/tools/crawl/scan/framework.js +645 -0
- package/dist/tools/crawl/scan/index.js +175 -0
- package/dist/tools/crawl/scan/project.js +180 -0
- package/dist/tools/crawl/scan/routes.js +354 -0
- package/dist/tools/crawl/scan/signals.js +414 -0
- package/dist/tools/method/cli.js +42 -0
- package/dist/tools/method/core/brief.js +302 -0
- package/dist/tools/method/core/extract.js +435 -0
- package/dist/tools/method/core/generate.js +504 -0
- package/dist/tools/method/core/lint.js +556 -0
- package/dist/tools/method/core/plan.js +405 -0
- package/dist/tools/method/core/prompt.js +150 -0
- package/dist/tools/method/core/questions.js +196 -0
- package/dist/tools/method/core/report.js +182 -0
- package/dist/tools/method/core/research-run.js +148 -0
- package/dist/tools/method/core/research.js +448 -0
- package/dist/tools/method/core/snippets.js +292 -0
- package/dist/tools/method/core/start.js +277 -0
- package/dist/tools/method/data/anatomy.js +141 -0
- package/dist/tools/method/data/appendices.js +202 -0
- package/dist/tools/method/data/cloudflare.js +192 -0
- package/dist/tools/method/data/evidence.js +137 -0
- package/dist/tools/method/data/geo-jobs.js +128 -0
- package/dist/tools/method/data/index.js +19 -0
- package/dist/tools/method/data/landing.js +67 -0
- package/dist/tools/method/data/lessons.js +127 -0
- package/dist/tools/method/data/measurement.js +142 -0
- package/dist/tools/method/data/method.js +141 -0
- package/dist/tools/method/data/offsite.js +148 -0
- package/dist/tools/method/data/pages.js +167 -0
- package/dist/tools/method/data/prompts.js +633 -0
- package/dist/tools/method/data/publish.js +113 -0
- package/dist/tools/method/data/render.js +117 -0
- package/dist/tools/method/data/research.js +131 -0
- package/dist/tools/method/data/roadmap.js +105 -0
- package/dist/tools/method/data/traps.js +74 -0
- package/dist/tools/method/data/verify.js +88 -0
- package/dist/tools/method/data/viral-list.js +39 -0
- package/dist/tools/method/index.js +209 -0
- package/dist/tools/method/lib/calls.js +53 -0
- package/dist/tools/method/lib/framework.js +525 -0
- package/dist/tools/method/lib/l10n.js +17 -0
- package/dist/tools/method/lib/routes.js +118 -0
- package/dist/tools/method/lib/state.js +454 -0
- package/dist/tools/method/lib/text.js +165 -0
- package/dist/tools/method/mcp/prompts.js +79 -0
- package/dist/tools/method/mcp/resources.js +50 -0
- package/dist/tools/visibility/analyze.js +463 -0
- package/dist/tools/visibility/cli.js +86 -0
- package/dist/tools/visibility/estimate.js +105 -0
- package/dist/tools/visibility/index.js +108 -0
- package/dist/tools/visibility/judge.js +156 -0
- package/dist/tools/visibility/manual.js +275 -0
- package/dist/tools/visibility/models.js +277 -0
- package/dist/tools/visibility/monitoring.js +165 -0
- package/dist/tools/visibility/openrouter.js +449 -0
- package/dist/tools/visibility/render.js +295 -0
- package/dist/tools/visibility/run.js +684 -0
- package/dist/tools/visibility/summary.js +164 -0
- package/dist/types.js +3 -0
- package/dist/util.js +92 -0
- package/package.json +54 -0
- package/server.json +34 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { foldKey, sameSite } from "./analyze.js";
|
|
2
|
+
export const emptyBucket = () => ({ answered: 0, mentioned: 0, recommendedFirst: 0, cited: 0 });
|
|
3
|
+
function addTo(b, a) {
|
|
4
|
+
b.answered++;
|
|
5
|
+
if (a.mentioned)
|
|
6
|
+
b.mentioned++;
|
|
7
|
+
if (a.recommendedFirst)
|
|
8
|
+
b.recommendedFirst++;
|
|
9
|
+
if (a.cited)
|
|
10
|
+
b.cited++;
|
|
11
|
+
}
|
|
12
|
+
export const ratio = (k, n) => (n > 0 ? Math.round((k / n) * 10000) / 10000 : 0);
|
|
13
|
+
/** Counts toward the rates: answered, not diagnostic, not a question that names the brand. */
|
|
14
|
+
export function isScored(a, branded = new Set()) {
|
|
15
|
+
return !a.error && a.questionType !== "diagnostic" && !branded.has(a.questionId);
|
|
16
|
+
}
|
|
17
|
+
export function summarize(answers, o) {
|
|
18
|
+
const branded = o.branded ?? new Set();
|
|
19
|
+
const scored = answers.filter((a) => isScored(a, branded));
|
|
20
|
+
const all = emptyBucket();
|
|
21
|
+
const byFamily = {};
|
|
22
|
+
const byLanguage = {};
|
|
23
|
+
for (const a of scored) {
|
|
24
|
+
addTo(all, a);
|
|
25
|
+
addTo((byFamily[a.family] ??= emptyBucket()), a);
|
|
26
|
+
addTo((byLanguage[a.lang ?? "?"] ??= emptyBucket()), a);
|
|
27
|
+
}
|
|
28
|
+
const reporters = new Set(answers.filter((a) => !a.error && a.citations.length > 0).map((a) => a.family));
|
|
29
|
+
const citationScored = scored.filter((a) => reporters.has(a.family));
|
|
30
|
+
const citedCount = citationScored.filter((a) => a.cited).length;
|
|
31
|
+
const citationBlind = [...new Set(scored.map((a) => a.family))].filter((f) => !reporters.has(f));
|
|
32
|
+
// Competitors: who is named instead (per answer, deduped), research names are canonical.
|
|
33
|
+
const comp = new Map();
|
|
34
|
+
for (const a of scored) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const names = [...a.namedProducts, ...a.competitorsNamed];
|
|
37
|
+
for (const raw of names) {
|
|
38
|
+
if (!raw || o.isBrand(raw))
|
|
39
|
+
continue;
|
|
40
|
+
const canonical = o.canonicalCompetitor?.(raw);
|
|
41
|
+
const display = canonical ?? raw.trim();
|
|
42
|
+
const key = foldKey(display);
|
|
43
|
+
if (!key || seen.has(key))
|
|
44
|
+
continue;
|
|
45
|
+
seen.add(key);
|
|
46
|
+
const e = comp.get(key) ?? { count: 0, spellings: new Map(), canonical };
|
|
47
|
+
e.count++;
|
|
48
|
+
e.spellings.set(display, (e.spellings.get(display) ?? 0) + 1);
|
|
49
|
+
if (canonical)
|
|
50
|
+
e.canonical = canonical;
|
|
51
|
+
comp.set(key, e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const topCompetitors = [...comp.values()]
|
|
55
|
+
.map((e) => ({
|
|
56
|
+
name: e.canonical ?? [...e.spellings.entries()].sort((x, y) => y[1] - x[1] || x[0].localeCompare(y[0]))[0][0],
|
|
57
|
+
count: e.count,
|
|
58
|
+
known: !!e.canonical,
|
|
59
|
+
}))
|
|
60
|
+
.sort((x, y) => y.count - x.count || x.name.localeCompare(y.name));
|
|
61
|
+
const domains = new Map();
|
|
62
|
+
for (const a of scored) {
|
|
63
|
+
const ds = new Set(a.citations.map((c) => c.domain).filter((d) => d && !o.ownDomains.some((own) => sameSite(d, own))));
|
|
64
|
+
for (const d of ds)
|
|
65
|
+
domains.set(d, (domains.get(d) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
const topCitedDomains = [...domains.entries()]
|
|
68
|
+
.map(([domain, count]) => ({ domain, count }))
|
|
69
|
+
.sort((x, y) => y.count - x.count || x.domain.localeCompare(y.domain))
|
|
70
|
+
.slice(0, 15);
|
|
71
|
+
const diag = answers.filter((a) => !a.error && a.questionType === "diagnostic");
|
|
72
|
+
const stabilityMap = new Map();
|
|
73
|
+
for (const a of scored) {
|
|
74
|
+
const k = `${a.questionId}\u0000${a.family}`;
|
|
75
|
+
const s = stabilityMap.get(k) ?? { questionId: a.questionId, family: a.family, samples: 0, mentioned: 0, cited: 0 };
|
|
76
|
+
s.samples++;
|
|
77
|
+
if (a.mentioned)
|
|
78
|
+
s.mentioned++;
|
|
79
|
+
if (a.cited)
|
|
80
|
+
s.cited++;
|
|
81
|
+
stabilityMap.set(k, s);
|
|
82
|
+
}
|
|
83
|
+
const stability = [...stabilityMap.values()].some((s) => s.samples > 1) ? [...stabilityMap.values()] : [];
|
|
84
|
+
return {
|
|
85
|
+
summary: {
|
|
86
|
+
answered: all.answered,
|
|
87
|
+
mentionRate: ratio(all.mentioned, all.answered),
|
|
88
|
+
recommendedFirstRate: ratio(all.recommendedFirst, all.answered),
|
|
89
|
+
citationRate: ratio(citedCount, citationScored.length),
|
|
90
|
+
byFamily,
|
|
91
|
+
topCompetitors: topCompetitors.slice(0, 10).map(({ name, count }) => ({ name, count })),
|
|
92
|
+
topCitedDomains,
|
|
93
|
+
},
|
|
94
|
+
extras: {
|
|
95
|
+
citationBlind,
|
|
96
|
+
citationDenominator: citationScored.length,
|
|
97
|
+
citedCount,
|
|
98
|
+
byLanguage,
|
|
99
|
+
errors: answers.filter((a) => a.error && !/^(skipped|not run)/i.test(a.error)).length,
|
|
100
|
+
skipped: answers.filter((a) => a.error && /^(skipped|not run)/i.test(a.error)).length,
|
|
101
|
+
unsearched: answers.filter((a) => !a.error && a.searched === false).length,
|
|
102
|
+
diagnostics: {
|
|
103
|
+
answered: diag.length,
|
|
104
|
+
mentioned: diag.filter((a) => a.mentioned).length,
|
|
105
|
+
identityCorrect: diag.filter((a) => a.identityCorrect === true).length,
|
|
106
|
+
identityWrong: diag.filter((a) => a.identityCorrect === false).length,
|
|
107
|
+
identityUnknown: diag.filter((a) => a.identityCorrect === null || a.identityCorrect === undefined).length,
|
|
108
|
+
cited: diag.filter((a) => a.cited).length,
|
|
109
|
+
},
|
|
110
|
+
stability,
|
|
111
|
+
newCompetitors: topCompetitors.filter((c) => !c.known).slice(0, 10).map((c) => c.name),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function diffRuns(prev, curr, branded = new Set()) {
|
|
116
|
+
const key = (a) => `${a.questionId}\u0000${a.family}`;
|
|
117
|
+
const group = (run) => {
|
|
118
|
+
const m = new Map();
|
|
119
|
+
for (const a of run.answers)
|
|
120
|
+
if (isScored(a, branded))
|
|
121
|
+
m.set(key(a), [...(m.get(key(a)) ?? []), a]);
|
|
122
|
+
return m;
|
|
123
|
+
};
|
|
124
|
+
const p = group(prev);
|
|
125
|
+
const c = group(curr);
|
|
126
|
+
const common = [...c.keys()].filter((k) => p.has(k));
|
|
127
|
+
const rates = (m, keys) => {
|
|
128
|
+
const r = { answered: 0, mentioned: 0, recommendedFirst: 0, cited: 0 };
|
|
129
|
+
for (const k of keys)
|
|
130
|
+
for (const a of m.get(k) ?? [])
|
|
131
|
+
addTo(r, a);
|
|
132
|
+
return r;
|
|
133
|
+
};
|
|
134
|
+
const byFamily = {};
|
|
135
|
+
for (const fam of new Set(common.map((k) => k.split("\u0000")[1]))) {
|
|
136
|
+
const ks = common.filter((k) => k.split("\u0000")[1] === fam);
|
|
137
|
+
byFamily[fam] = { before: rates(p, ks), after: rates(c, ks) };
|
|
138
|
+
}
|
|
139
|
+
const anyMentioned = (xs) => (xs ?? []).some((a) => a.mentioned);
|
|
140
|
+
const split = (k) => {
|
|
141
|
+
const [questionId, family] = k.split("\u0000");
|
|
142
|
+
return { questionId, family };
|
|
143
|
+
};
|
|
144
|
+
const ids = (run) => new Set(run.models.map((m) => m.id));
|
|
145
|
+
const qs = (run) => new Set(run.answers.map((a) => a.questionId));
|
|
146
|
+
const sameSet = (a, b) => a.size === b.size && [...a].every((x) => b.has(x));
|
|
147
|
+
return {
|
|
148
|
+
previousId: prev.id,
|
|
149
|
+
previousTs: prev.ts,
|
|
150
|
+
pairs: common.length,
|
|
151
|
+
sameModels: sameSet(ids(prev), ids(curr)),
|
|
152
|
+
sameQuestions: sameSet(qs(prev), qs(curr)),
|
|
153
|
+
before: rates(p, common),
|
|
154
|
+
after: rates(c, common),
|
|
155
|
+
byFamily,
|
|
156
|
+
gained: common.filter((k) => !anyMentioned(p.get(k)) && anyMentioned(c.get(k))).map(split),
|
|
157
|
+
lost: common.filter((k) => anyMentioned(p.get(k)) && !anyMentioned(c.get(k))).map(split),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function addDays(iso, days) {
|
|
161
|
+
const d = new Date(iso);
|
|
162
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
163
|
+
return d.toISOString().slice(0, 10);
|
|
164
|
+
}
|
package/dist/types.js
ADDED
package/dist/util.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Standard successful tool result: markdown for the model + optional structured data. */
|
|
2
|
+
export function ok(text, structured) {
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text }],
|
|
5
|
+
...(structured ? { structuredContent: structured } : {}),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function fail(text) {
|
|
9
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
10
|
+
}
|
|
11
|
+
export function mdTable(headers, rows) {
|
|
12
|
+
const esc = (v) => String(v ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
13
|
+
return [
|
|
14
|
+
`| ${headers.map(esc).join(" | ")} |`,
|
|
15
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
16
|
+
...rows.map((r) => `| ${r.map(esc).join(" | ")} |`),
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
export function truncate(s, n) {
|
|
20
|
+
return s.length <= n ? s : s.slice(0, Math.max(0, n - 1)) + "…";
|
|
21
|
+
}
|
|
22
|
+
export function domainOf(url) {
|
|
23
|
+
try {
|
|
24
|
+
return new URL(url).hostname.replace(/^www\./, "").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return url.replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/)[0].toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Normalise a user-supplied site URL to https://host (no trailing slash). */
|
|
31
|
+
export function normalizeSiteUrl(input) {
|
|
32
|
+
let s = input.trim();
|
|
33
|
+
if (!/^https?:\/\//i.test(s))
|
|
34
|
+
s = "https://" + s;
|
|
35
|
+
const u = new URL(s);
|
|
36
|
+
return `${u.protocol}//${u.host}`;
|
|
37
|
+
}
|
|
38
|
+
const SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
39
|
+
export function sortFindings(fs) {
|
|
40
|
+
return [...fs].sort((a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity]);
|
|
41
|
+
}
|
|
42
|
+
export function findingsToMarkdown(fs, limit = 50) {
|
|
43
|
+
if (!fs.length)
|
|
44
|
+
return "No issues found.";
|
|
45
|
+
return sortFindings(fs)
|
|
46
|
+
.slice(0, limit)
|
|
47
|
+
.map((f, i) => {
|
|
48
|
+
const lines = [`${i + 1}. **[${f.severity.toUpperCase()}] ${f.title}** (gate: ${f.gate}${f.prompt ? `, fix with ${f.prompt}` : ""})`, ` ${f.detail}`];
|
|
49
|
+
if (f.files?.length)
|
|
50
|
+
lines.push(` Files: ${f.files.slice(0, 8).join(", ")}`);
|
|
51
|
+
if (f.urls?.length)
|
|
52
|
+
lines.push(` URLs: ${f.urls.slice(0, 8).join(", ")}`);
|
|
53
|
+
if (f.fix)
|
|
54
|
+
lines.push(` Fix: ${f.fix}`);
|
|
55
|
+
if (f.accept)
|
|
56
|
+
lines.push(` Accept when: ${f.accept}`);
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The brand's one sentence from its four parts (brings → we do → gets → fear is unnecessary),
|
|
63
|
+
* written the way the book does: "Upload your invoice. We move the line items … Your document is never stored."
|
|
64
|
+
* A part that starts with a continuation word ("and", "so", "ve", ",") continues the previous sentence;
|
|
65
|
+
* otherwise the previous part is closed with a period and the next one is capitalised.
|
|
66
|
+
* Parts still holding [FILL]/[DOLDUR] are skipped; returns undefined when fewer than `minParts` remain.
|
|
67
|
+
*/
|
|
68
|
+
export function composeOneSentence(o, minParts = 2) {
|
|
69
|
+
if (!o)
|
|
70
|
+
return undefined;
|
|
71
|
+
const parts = [o.brings, o.weDo, o.gets, o.fear]
|
|
72
|
+
.map((x) => (x ?? "").trim())
|
|
73
|
+
.filter((x) => x && !/\[(FILL|DOLDUR)\]/i.test(x));
|
|
74
|
+
if (parts.length < minParts)
|
|
75
|
+
return undefined;
|
|
76
|
+
const CONTINUES = /^(?:[,;:–—-]|(?:and|so|then|but|or|while|which|that|plus|ve|ama|sonra|ya da|veya|böylece)\b)/iu;
|
|
77
|
+
const cap = (x) => x.charAt(0).toLocaleUpperCase() + x.slice(1);
|
|
78
|
+
let out = cap(parts[0]);
|
|
79
|
+
for (const next of parts.slice(1)) {
|
|
80
|
+
if (CONTINUES.test(next) && !/[.!?…]$/.test(out)) {
|
|
81
|
+
out += /^[,;:]/.test(next) ? next : ` ${next}`;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (!/[.!?…]$/.test(out))
|
|
85
|
+
out += ".";
|
|
86
|
+
out += ` ${cap(next)}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!/[.!?…]$/.test(out))
|
|
90
|
+
out += ".";
|
|
91
|
+
return out;
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "katman-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Katman MCP: makes your vibe-coded site readable, quotable and recommendable by ChatGPT, Claude, Gemini and Perplexity. Scans your codebase, audits your live site like AI crawlers do, measures AI visibility with your own OpenRouter key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"katman-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"server.json"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json && node scripts/postbuild.mjs",
|
|
21
|
+
"dev": "tsx src/index.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"geo",
|
|
29
|
+
"aeo",
|
|
30
|
+
"seo",
|
|
31
|
+
"llm-seo",
|
|
32
|
+
"ai-visibility",
|
|
33
|
+
"chatgpt",
|
|
34
|
+
"vibe-coding",
|
|
35
|
+
"cursor",
|
|
36
|
+
"claude-code"
|
|
37
|
+
],
|
|
38
|
+
"homepage": "https://katman.pro",
|
|
39
|
+
"license": "UNLICENSED",
|
|
40
|
+
"author": "Rnify Limited",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
43
|
+
"node-html-parser": "^9.0.4",
|
|
44
|
+
"robots-parser": "^3.0.1",
|
|
45
|
+
"zod": "^4.6.5"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^26.6.3",
|
|
49
|
+
"tsx": "^4.23.15",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^4.1.11"
|
|
52
|
+
},
|
|
53
|
+
"mcpName": "pro.katman/mcp"
|
|
54
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "pro.katman/mcp",
|
|
4
|
+
"title": "Katman",
|
|
5
|
+
"description": "Get your vibe-coded site read, quoted and recommended by ChatGPT, Claude, Gemini and Perplexity.",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"websiteUrl": "https://katman.pro/mcp",
|
|
8
|
+
"packages": [
|
|
9
|
+
{
|
|
10
|
+
"registryType": "npm",
|
|
11
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
12
|
+
"identifier": "katman-mcp",
|
|
13
|
+
"version": "0.1.0",
|
|
14
|
+
"runtimeHint": "npx",
|
|
15
|
+
"transport": { "type": "stdio" },
|
|
16
|
+
"environmentVariables": [
|
|
17
|
+
{
|
|
18
|
+
"name": "KATMAN_KEY",
|
|
19
|
+
"description": "Optional. Your Katman Pro key from katman.pro/app (7-day free trial, no card). Unlocks the Pro tools; the free diagnosis tools work without it.",
|
|
20
|
+
"isRequired": false,
|
|
21
|
+
"isSecret": true,
|
|
22
|
+
"format": "string"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "OPENROUTER_API_KEY",
|
|
26
|
+
"description": "Optional. Your own OpenRouter key, used only for AI-visibility runs.",
|
|
27
|
+
"isRequired": false,
|
|
28
|
+
"isSecret": true,
|
|
29
|
+
"format": "string"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|