seo-gravity-mcp 1.0.0 → 1.0.1
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/dist/utils/nlp.js +42 -7
- package/package.json +1 -4
package/dist/utils/nlp.js
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Native lightweight word tokenizer
|
|
2
|
+
function tokenize(text) {
|
|
3
|
+
return text.match(/[a-zA-Z0-9'-]+/g) || [];
|
|
4
|
+
}
|
|
5
|
+
// Built-in TF-IDF implementation without heavy external dependencies
|
|
6
|
+
class SimpleTfIdf {
|
|
7
|
+
docs = [];
|
|
8
|
+
addDocument(doc) {
|
|
9
|
+
const rawTokens = tokenize(doc.toLowerCase());
|
|
10
|
+
const tokens = rawTokens.filter(t => t.length >= 3 && !stopwords.has(t) && !/^\d+$/.test(t));
|
|
11
|
+
this.docs.push(tokens);
|
|
12
|
+
}
|
|
13
|
+
listTerms(docIndex) {
|
|
14
|
+
if (docIndex < 0 || docIndex >= this.docs.length)
|
|
15
|
+
return [];
|
|
16
|
+
const docTokens = this.docs[docIndex];
|
|
17
|
+
if (docTokens.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const totalDocs = this.docs.length;
|
|
20
|
+
const termCounts = new Map();
|
|
21
|
+
for (const t of docTokens) {
|
|
22
|
+
termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const [term, count] of termCounts.entries()) {
|
|
26
|
+
const tf = count / docTokens.length;
|
|
27
|
+
let docFreq = 0;
|
|
28
|
+
for (const d of this.docs) {
|
|
29
|
+
if (d.includes(term))
|
|
30
|
+
docFreq++;
|
|
31
|
+
}
|
|
32
|
+
const idf = Math.log((1 + totalDocs) / (1 + docFreq)) + 1;
|
|
33
|
+
results.push({ term, tfidf: tf * idf * 10 });
|
|
34
|
+
}
|
|
35
|
+
return results.sort((a, b) => b.tfidf - a.tfidf);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
3
38
|
const stopwords = new Set([
|
|
4
39
|
'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'aren\'t', 'as', 'at',
|
|
5
40
|
'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by', 'can', 'can\'t', 'cannot',
|
|
@@ -21,7 +56,7 @@ const stopwords = new Set([
|
|
|
21
56
|
*/
|
|
22
57
|
export function extractKeyphrases(text, maxItems = 30) {
|
|
23
58
|
const clean = text.toLowerCase().replace(/[^a-z0-9\s-]/g, ' ');
|
|
24
|
-
const rawTokens =
|
|
59
|
+
const rawTokens = tokenize(clean);
|
|
25
60
|
const tokens = rawTokens.filter(t => t.length > 2 && !stopwords.has(t) && !/^\d+$/.test(t));
|
|
26
61
|
const totalTokens = tokens.length || 1;
|
|
27
62
|
const phraseCounts = new Map();
|
|
@@ -54,7 +89,7 @@ export function extractKeyphrases(text, maxItems = 30) {
|
|
|
54
89
|
* Computes TF-IDF between target text and a set of competitor texts to detect content gaps.
|
|
55
90
|
*/
|
|
56
91
|
export function computeContentGapTfIdf(targetText, competitorTexts) {
|
|
57
|
-
const tfidf = new
|
|
92
|
+
const tfidf = new SimpleTfIdf();
|
|
58
93
|
// Doc 0 is target text
|
|
59
94
|
tfidf.addDocument(targetText);
|
|
60
95
|
// Docs 1..N are competitors
|
|
@@ -75,7 +110,7 @@ export function computeContentGapTfIdf(targetText, competitorTexts) {
|
|
|
75
110
|
});
|
|
76
111
|
}
|
|
77
112
|
// Find target frequencies
|
|
78
|
-
const targetTokens =
|
|
113
|
+
const targetTokens = tokenize(targetText.toLowerCase()).filter(t => !stopwords.has(t));
|
|
79
114
|
const targetTokenCounts = new Map();
|
|
80
115
|
for (const t of targetTokens) {
|
|
81
116
|
targetTokenCounts.set(t, (targetTokenCounts.get(t) || 0) + 1);
|
|
@@ -104,7 +139,7 @@ export function computeContentGapTfIdf(targetText, competitorTexts) {
|
|
|
104
139
|
export function calculateReadability(text) {
|
|
105
140
|
const clean = text.replace(/\s+/g, ' ').trim();
|
|
106
141
|
const sentences = clean.split(/[.!?]+/).filter(s => s.trim().length > 0);
|
|
107
|
-
const words =
|
|
142
|
+
const words = tokenize(clean).filter(w => /[a-zA-Z]/.test(w));
|
|
108
143
|
const numSentences = Math.max(sentences.length, 1);
|
|
109
144
|
const numWords = Math.max(words.length, 1);
|
|
110
145
|
let numSyllables = 0;
|
|
@@ -129,7 +164,7 @@ export function calculateReadability(text) {
|
|
|
129
164
|
// Long sentences (> 25 words)
|
|
130
165
|
const longSentences = sentences
|
|
131
166
|
.map(s => s.trim())
|
|
132
|
-
.filter(s =>
|
|
167
|
+
.filter(s => tokenize(s).length > 25);
|
|
133
168
|
return {
|
|
134
169
|
fleschReadingEase: Math.max(0, Math.min(100, fleschReadingEase)),
|
|
135
170
|
gradeLevel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seo-gravity-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Comprehensive Next-Gen SEO, GEO & Competitor Intelligence MCP Server for Antigravity",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,15 +32,12 @@
|
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
33
33
|
"axios": "^1.7.9",
|
|
34
34
|
"cheerio": "^1.0.0",
|
|
35
|
-
"dotenv": "^16.4.7",
|
|
36
35
|
"fast-xml-parser": "^5.0.8",
|
|
37
36
|
"jsdom": "^26.0.0",
|
|
38
|
-
"natural": "^8.0.1",
|
|
39
37
|
"zod": "^3.24.2"
|
|
40
38
|
},
|
|
41
39
|
"devDependencies": {
|
|
42
40
|
"@types/jsdom": "^21.1.7",
|
|
43
|
-
"@types/natural": "^5.1.5",
|
|
44
41
|
"@types/node": "^22.13.5",
|
|
45
42
|
"ts-node": "^10.9.2",
|
|
46
43
|
"typescript": "^5.7.3"
|