visibilia-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/README.md +76 -0
- package/dist/index.js +179 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# visibilia-mcp — Brand Pulse MCP server
|
|
2
|
+
|
|
3
|
+
Conecta cualquier cliente MCP (Claude Desktop, Cline, Cursor MCP) con tu cuenta de [VisibilIA](https://visibilia.app) para preguntar en lenguaje natural cómo aparece tu marca en los motores de IA.
|
|
4
|
+
|
|
5
|
+
## Qué hace
|
|
6
|
+
|
|
7
|
+
Expone 5 herramientas MCP que llaman a la API REST de VisibilIA con tu Bearer token:
|
|
8
|
+
|
|
9
|
+
| Tool | Qué responde |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `get_brand_visibility` | Scores de mention rate, share of voice, sentiment, citation y composite por LLM |
|
|
12
|
+
| `list_intents` | Tus intents (topics) trackeados |
|
|
13
|
+
| `list_top_cited_sources` | Top dominios que cita la IA sobre tu sector (últimos 30 días) |
|
|
14
|
+
| `list_recent_mentions` | Menciones recientes con sentiment y posición |
|
|
15
|
+
| `compare_with_competitor` | Tu share of voice vs un competidor concreto |
|
|
16
|
+
|
|
17
|
+
## Setup en Claude Desktop
|
|
18
|
+
|
|
19
|
+
1. Genera una API key en https://visibilia.app/app/settings/api (requiere plan Starter o Pro).
|
|
20
|
+
2. Edita `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"visibilia": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "visibilia-mcp"],
|
|
28
|
+
"env": {
|
|
29
|
+
"VISIBILIA_API_KEY": "vsk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Reinicia Claude Desktop. Verás un nuevo set de herramientas con prefijo `visibilia_*` en el selector de tools.
|
|
37
|
+
|
|
38
|
+
Ejemplos de prompt en Claude Desktop una vez conectado:
|
|
39
|
+
|
|
40
|
+
- "Resúmeme la visibilidad de mi marca esta semana"
|
|
41
|
+
- "¿Qué dominios cita la IA más sobre mi sector?"
|
|
42
|
+
- "Compara mi share of voice con [competidor X] estos últimos 7 días"
|
|
43
|
+
|
|
44
|
+
## Setup en otros clientes
|
|
45
|
+
|
|
46
|
+
El servidor habla MCP estándar sobre stdio. Cualquier cliente que soporte `command + args + env` lo puede usar.
|
|
47
|
+
|
|
48
|
+
## Self-hosted / API base personalizada
|
|
49
|
+
|
|
50
|
+
Si tienes una instalación on-prem de VisibilIA, pasa también `VISIBILIA_API_BASE`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"env": {
|
|
54
|
+
"VISIBILIA_API_KEY": "vsk_live_...",
|
|
55
|
+
"VISIBILIA_API_BASE": "https://visibilia.midominio.com"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Rate limits
|
|
60
|
+
|
|
61
|
+
- Starter: 60 requests/minuto por API key
|
|
62
|
+
- Pro: 240 requests/minuto por API key
|
|
63
|
+
|
|
64
|
+
El servidor reporta el error de rate limit de vuelta al cliente MCP con un mensaje claro.
|
|
65
|
+
|
|
66
|
+
## Desarrollo local
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm install
|
|
70
|
+
pnpm dev # tsx src/index.ts
|
|
71
|
+
pnpm build # genera dist/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Licencia
|
|
75
|
+
|
|
76
|
+
MIT. Repo principal en https://github.com/JpegzaCreate/visibilia.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VisibilIA MCP Server — Brand Pulse.
|
|
4
|
+
*
|
|
5
|
+
* Exposes 5 tools that any MCP client (Claude Desktop, Cline, Cursor MCP)
|
|
6
|
+
* can call to inspect your brand's visibility in AI generative engines:
|
|
7
|
+
*
|
|
8
|
+
* - get_brand_visibility — latest scores + share of voice
|
|
9
|
+
* - list_intents — your tracked search intents
|
|
10
|
+
* - list_top_cited_sources — which URLs the LLMs cite about your sector
|
|
11
|
+
* - list_recent_mentions — last N mentions (with sentiment)
|
|
12
|
+
* - compare_with_competitor — head-to-head SoV vs a named competitor
|
|
13
|
+
*
|
|
14
|
+
* Auth via Bearer token (the same API key you generate in
|
|
15
|
+
* /app/settings/api). Pass it via env var VISIBILIA_API_KEY.
|
|
16
|
+
*
|
|
17
|
+
* Transport: stdio. Configure in Claude Desktop:
|
|
18
|
+
*
|
|
19
|
+
* {
|
|
20
|
+
* "mcpServers": {
|
|
21
|
+
* "visibilia": {
|
|
22
|
+
* "command": "npx",
|
|
23
|
+
* "args": ["-y", "@visibilia/mcp"],
|
|
24
|
+
* "env": { "VISIBILIA_API_KEY": "vsk_live_..." }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
30
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
const API_KEY = process.env.VISIBILIA_API_KEY;
|
|
33
|
+
const API_BASE = process.env.VISIBILIA_API_BASE?.replace(/\/+$/, "") ??
|
|
34
|
+
"https://visibilia.app";
|
|
35
|
+
if (!API_KEY) {
|
|
36
|
+
// Don't crash before the SDK handshake — log to stderr (stdio reserves
|
|
37
|
+
// stdout for protocol traffic) and continue. Tools will return a clear
|
|
38
|
+
// error on each call so the user sees it in Claude Desktop.
|
|
39
|
+
console.error("[visibilia-mcp] VISIBILIA_API_KEY not set. Generate one at https://visibilia.app/app/settings/api and re-launch.");
|
|
40
|
+
}
|
|
41
|
+
async function callApi(path, params = {}) {
|
|
42
|
+
if (!API_KEY) {
|
|
43
|
+
return {
|
|
44
|
+
error: "VISIBILIA_API_KEY no configurada. Genera una key en https://visibilia.app/app/settings/api y añádela al env del MCP server.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const url = new URL(`${API_BASE}${path}`);
|
|
48
|
+
for (const [k, v] of Object.entries(params)) {
|
|
49
|
+
if (v)
|
|
50
|
+
url.searchParams.set(k, v);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
55
|
+
});
|
|
56
|
+
if (res.status === 401) {
|
|
57
|
+
return {
|
|
58
|
+
error: "API key inválida o revocada. Revisa en /app/settings/api o regenera una nueva.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (res.status === 429) {
|
|
62
|
+
return {
|
|
63
|
+
error: "Rate limit excedido (60 req/min Starter, 240 req/min Pro). Espera un minuto y reintenta.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
return { error: `API error ${res.status}: ${(await res.text()).slice(0, 200)}` };
|
|
68
|
+
}
|
|
69
|
+
return (await res.json());
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
error: err instanceof Error
|
|
74
|
+
? `Transport error: ${err.message}`
|
|
75
|
+
: "Transport error",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function asText(obj) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const server = new McpServer({
|
|
85
|
+
name: "visibilia",
|
|
86
|
+
version: "0.1.0",
|
|
87
|
+
});
|
|
88
|
+
server.tool("get_brand_visibility", "Latest visibility scores (mention_rate, share_of_voice, sentiment, citation, composite) per LLM provider for your active brand. Use this to answer questions like 'how visible is my brand in AI this week?'", {
|
|
89
|
+
intent_id: z
|
|
90
|
+
.string()
|
|
91
|
+
.uuid()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Filter to a specific intent UUID. Omit for org-wide."),
|
|
94
|
+
from: z
|
|
95
|
+
.string()
|
|
96
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("ISO date (YYYY-MM-DD). Defaults to last 7 days."),
|
|
99
|
+
to: z
|
|
100
|
+
.string()
|
|
101
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("ISO date (YYYY-MM-DD). Defaults to today."),
|
|
104
|
+
}, async (args) => {
|
|
105
|
+
const from = args.from ??
|
|
106
|
+
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
107
|
+
.toISOString()
|
|
108
|
+
.slice(0, 10);
|
|
109
|
+
const params = { from, limit: "100" };
|
|
110
|
+
if (args.intent_id)
|
|
111
|
+
params.intent_id = args.intent_id;
|
|
112
|
+
if (args.to)
|
|
113
|
+
params.to = args.to;
|
|
114
|
+
const out = await callApi("/api/v1/scores", params);
|
|
115
|
+
return asText(out);
|
|
116
|
+
});
|
|
117
|
+
server.tool("list_intents", "List the search intents (topics) tracked for your active brand. Each intent has prompts that get scanned across LLMs.", {}, async () => {
|
|
118
|
+
const out = await callApi("/api/v1/intents", { limit: "100" });
|
|
119
|
+
return asText(out);
|
|
120
|
+
});
|
|
121
|
+
server.tool("list_top_cited_sources", "Top domains the LLMs cite when answering about your sector. Helpful to know where you need authority (e.g. 'Reddit and YouTube dominate, you should target them').", {
|
|
122
|
+
from: z
|
|
123
|
+
.string()
|
|
124
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("ISO date. Defaults to last 30 days."),
|
|
127
|
+
}, async (args) => {
|
|
128
|
+
const from = args.from ??
|
|
129
|
+
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
|
130
|
+
.toISOString()
|
|
131
|
+
.slice(0, 10);
|
|
132
|
+
const out = await callApi("/api/v1/citations", {
|
|
133
|
+
from,
|
|
134
|
+
group_by: "domain",
|
|
135
|
+
limit: "20",
|
|
136
|
+
});
|
|
137
|
+
return asText(out);
|
|
138
|
+
});
|
|
139
|
+
server.tool("list_recent_mentions", "Recent brand mentions detected in LLM responses, with sentiment + position + citation URLs. Useful for 'what are people saying about us'.", {
|
|
140
|
+
intent_id: z.string().uuid().optional(),
|
|
141
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
142
|
+
}, async (args) => {
|
|
143
|
+
const params = { limit: String(args.limit ?? 20) };
|
|
144
|
+
if (args.intent_id)
|
|
145
|
+
params.intent_id = args.intent_id;
|
|
146
|
+
const out = await callApi("/api/v1/mentions", params);
|
|
147
|
+
return asText(out);
|
|
148
|
+
});
|
|
149
|
+
server.tool("compare_with_competitor", "Compare your brand's share of voice against a specific competitor for the last 7 days, per LLM provider.", {
|
|
150
|
+
competitor_name: z.string().min(1).max(120),
|
|
151
|
+
}, async (args) => {
|
|
152
|
+
// We don't have a dedicated comparison endpoint yet, so we approximate
|
|
153
|
+
// with two queries: get scores (your SoV) and get mentions filtered by
|
|
154
|
+
// competitor_id is not null. The Claude client can then reason over
|
|
155
|
+
// both JSON blobs. Future: dedicated /api/v1/competitors endpoint.
|
|
156
|
+
const from = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
157
|
+
.toISOString()
|
|
158
|
+
.slice(0, 10);
|
|
159
|
+
const [scores, mentions] = await Promise.all([
|
|
160
|
+
callApi("/api/v1/scores", { from, limit: "100" }),
|
|
161
|
+
callApi("/api/v1/mentions", { limit: "100" }),
|
|
162
|
+
]);
|
|
163
|
+
return asText({
|
|
164
|
+
hint: `Filtra mentions por competitor_id no nulo y match_text que contenga "${args.competitor_name}" para calcular su share. Tu propia SoV está en scores.share_of_voice.`,
|
|
165
|
+
competitor_name: args.competitor_name,
|
|
166
|
+
from,
|
|
167
|
+
your_scores: scores,
|
|
168
|
+
raw_mentions: mentions,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
async function main() {
|
|
172
|
+
const transport = new StdioServerTransport();
|
|
173
|
+
await server.connect(transport);
|
|
174
|
+
console.error("[visibilia-mcp] connected via stdio");
|
|
175
|
+
}
|
|
176
|
+
main().catch((err) => {
|
|
177
|
+
console.error("[visibilia-mcp] fatal:", err);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "visibilia-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for VisibilIA — query your brand visibility in ChatGPT/Gemini/Perplexity/Claude from any MCP client (Claude Desktop, Cline, etc).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"visibilia-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "pnpm build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"visibilia",
|
|
23
|
+
"geo",
|
|
24
|
+
"ai-visibility",
|
|
25
|
+
"llm",
|
|
26
|
+
"chatgpt",
|
|
27
|
+
"perplexity",
|
|
28
|
+
"gemini",
|
|
29
|
+
"claude"
|
|
30
|
+
],
|
|
31
|
+
"author": "VisibilIA",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://visibilia.app",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/JpegzaCreate/visibilia.git",
|
|
37
|
+
"directory": "mcp-server"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
|
+
"zod": "^3.23.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsx": "^4.0.0",
|
|
48
|
+
"typescript": "^5.4.0",
|
|
49
|
+
"@types/node": "^20.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|