@geogenio/mcp-server 1.0.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 ADDED
@@ -0,0 +1,116 @@
1
+ # GeoGen MCP Server
2
+
3
+ MCP (Model Context Protocol) server for the GeoGen LLM SEO tracking platform. Lets AI assistants query your LLM visibility data, manage entities, and analyze trends via natural language.
4
+
5
+ ## Setup
6
+
7
+ ### 1. Install & Build
8
+
9
+ ```bash
10
+ cd mcp-server
11
+ npm install
12
+ npm run build
13
+ ```
14
+
15
+ ### 2. Get Your API Key
16
+
17
+ Go to your GeoGen workspace **Settings > API Keys** and create a new key.
18
+
19
+ ### 3. Configure Your AI Client
20
+
21
+ #### Claude Desktop
22
+
23
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "geogen": {
29
+ "command": "node",
30
+ "args": ["/absolute/path/to/mcp-server/build/index.js"],
31
+ "env": {
32
+ "GEOGEN_API_KEY": "your-api-key-here",
33
+ "GEOGEN_BASE_URL": "https://your-app.convex.site"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ #### Claude Code
41
+
42
+ ```bash
43
+ claude mcp add geogen -- node /absolute/path/to/mcp-server/build/index.js
44
+ ```
45
+
46
+ Then set the environment variables in your shell or `.env` file:
47
+ ```bash
48
+ export GEOGEN_API_KEY="your-api-key-here"
49
+ export GEOGEN_BASE_URL="https://your-app.convex.site"
50
+ ```
51
+
52
+ #### Cursor / Windsurf
53
+
54
+ Add to your MCP settings (see each IDE's documentation for the config location):
55
+
56
+ ```json
57
+ {
58
+ "geogen": {
59
+ "command": "node",
60
+ "args": ["/absolute/path/to/mcp-server/build/index.js"],
61
+ "env": {
62
+ "GEOGEN_API_KEY": "your-api-key-here",
63
+ "GEOGEN_BASE_URL": "https://your-app.convex.site"
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Available Tools
70
+
71
+ ### Read Operations
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `get_entities` | List all tracked websites/entities |
76
+ | `get_workspace` | Workspace usage, limits, credit balance |
77
+ | `get_workspace_members` | List team members |
78
+ | `get_workspace_tags` | List all tags |
79
+ | `get_models` | Available LLM models |
80
+ | `get_entity_prompts` | Prompts with stats for an entity |
81
+ | `get_responses` | LLM responses with mention status |
82
+ | `get_response_details` | Detailed response data (mentions, citations, fanouts) |
83
+ | `get_citations` | Top cited domains for an entity |
84
+ | `get_citation_details` | Detailed URLs for a cited domain |
85
+ | `get_competitors` | Competitor visibility leaderboard |
86
+ | `get_citations_trend` | Daily citation trend for top cited domains over time |
87
+ | `get_visibility_trend` | Visibility trend over time |
88
+ | `get_sentiment_trend` | Sentiment trend over time |
89
+ | `get_query_fanouts` | Web search queries LLMs perform |
90
+
91
+ ### Write Operations
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `create_entity` | Create a new tracked entity (consumes credits) |
96
+ | `add_prompts` | Add single or bulk prompts to an entity |
97
+ | `delete_prompt` | Delete a prompt |
98
+
99
+ ## Example Queries
100
+
101
+ Once configured, you can ask your AI assistant things like:
102
+
103
+ - *"List all my tracked entities"*
104
+ - *"What's the visibility trend for entity X over the last 30 days?"*
105
+ - *"Show me the top competitors for my website"*
106
+ - *"Which domains are LLMs citing when they talk about my brand?"*
107
+ - *"What search queries are LLMs running about my entity?"*
108
+ - *"Add a new tracking prompt: 'What is the best project management tool?'"*
109
+ - *"Give me a full SEO report comparing my entity against competitors"*
110
+
111
+ ## Environment Variables
112
+
113
+ | Variable | Required | Description |
114
+ |----------|----------|-------------|
115
+ | `GEOGEN_API_KEY` | Yes | Your GeoGen workspace API key |
116
+ | `GEOGEN_BASE_URL` | Yes | Your Convex site URL (e.g. `https://your-app.convex.site`) |
@@ -0,0 +1,118 @@
1
+ /**
2
+ * GeoGen API Client
3
+ * Wraps all /v1 REST endpoints for use by MCP tool handlers.
4
+ */
5
+ export class GeoGenApiClient {
6
+ baseUrl;
7
+ apiKey;
8
+ constructor(config) {
9
+ // Strip trailing slash from base URL
10
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
11
+ this.apiKey = config.apiKey;
12
+ }
13
+ async request(path, options) {
14
+ const url = `${this.baseUrl}${path}`;
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${this.apiKey}`,
20
+ ...options?.headers,
21
+ },
22
+ });
23
+ const data = await response.json();
24
+ if (!response.ok) {
25
+ const errorMsg = data?.error || data?.message || `HTTP ${response.status}`;
26
+ throw new Error(errorMsg);
27
+ }
28
+ return data;
29
+ }
30
+ buildQuery(params) {
31
+ const entries = Object.entries(params).filter((entry) => entry[1] !== undefined && entry[1] !== "");
32
+ if (entries.length === 0)
33
+ return "";
34
+ return "?" + new URLSearchParams(entries).toString();
35
+ }
36
+ // ── Entities ──────────────────────────────────────────────
37
+ async getEntities() {
38
+ return this.request("/v1/entities");
39
+ }
40
+ async createEntity(body) {
41
+ return this.request("/v1/entities", {
42
+ method: "POST",
43
+ body: JSON.stringify(body),
44
+ });
45
+ }
46
+ // ── Prompts ───────────────────────────────────────────────
47
+ async getEntityPrompts(params) {
48
+ const query = this.buildQuery(params);
49
+ return this.request(`/v1/entities/prompts${query}`);
50
+ }
51
+ async addPrompts(body) {
52
+ return this.request("/v1/entities/addprompt", {
53
+ method: "POST",
54
+ body: JSON.stringify(body),
55
+ });
56
+ }
57
+ async deletePrompt(promptId) {
58
+ const query = this.buildQuery({ promptId });
59
+ return this.request(`/v1/entities/deleteprompt${query}`, {
60
+ method: "DELETE",
61
+ });
62
+ }
63
+ // ── Workspace ─────────────────────────────────────────────
64
+ async getWorkspace() {
65
+ return this.request("/v1/workspace");
66
+ }
67
+ async getWorkspaceMembers() {
68
+ return this.request("/v1/workspace/members");
69
+ }
70
+ async getWorkspaceTags() {
71
+ return this.request("/v1/workspace/tags");
72
+ }
73
+ // ── Models ────────────────────────────────────────────────
74
+ async getModels() {
75
+ return this.request("/v1/models");
76
+ }
77
+ // ── Responses ─────────────────────────────────────────────
78
+ async getResponses(params) {
79
+ const query = this.buildQuery(params);
80
+ return this.request(`/v1/responses${query}`);
81
+ }
82
+ async getResponseDetails(params) {
83
+ const query = this.buildQuery(params);
84
+ return this.request(`/v1/responses/details${query}`);
85
+ }
86
+ // ── Citations ─────────────────────────────────────────────
87
+ async getCitations(params) {
88
+ const query = this.buildQuery(params);
89
+ return this.request(`/v1/entities/citations${query}`);
90
+ }
91
+ async getCitationsTrend(params) {
92
+ const query = this.buildQuery(params);
93
+ return this.request(`/v1/trends/citations${query}`);
94
+ }
95
+ async getCitationDetails(params) {
96
+ const query = this.buildQuery(params);
97
+ return this.request(`/v1/entities/citations/details${query}`);
98
+ }
99
+ // ── Competitors ───────────────────────────────────────────
100
+ async getCompetitors(params) {
101
+ const query = this.buildQuery(params);
102
+ return this.request(`/v1/competitors${query}`);
103
+ }
104
+ // ── Trends ────────────────────────────────────────────────
105
+ async getVisibilityTrend(params) {
106
+ const query = this.buildQuery(params);
107
+ return this.request(`/v1/trends/visibility${query}`);
108
+ }
109
+ async getSentimentTrend(params) {
110
+ const query = this.buildQuery(params);
111
+ return this.request(`/v1/trends/sentiment${query}`);
112
+ }
113
+ // ── Query Fanouts ─────────────────────────────────────────
114
+ async getQueryFanouts(params) {
115
+ const query = this.buildQuery(params);
116
+ return this.request(`/v1/query-fanouts${query}`);
117
+ }
118
+ }
package/build/index.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GeoGen MCP Server
4
+ *
5
+ * Exposes the GeoGen LLM SEO tracking API as MCP tools
6
+ * for AI assistants (Claude Desktop, Cursor, Windsurf, Claude Code, etc.)
7
+ *
8
+ * Configuration via environment variables:
9
+ * GEOGEN_API_KEY - Your GeoGen workspace API key (required)
10
+ * GEOGEN_BASE_URL - Your Convex site URL (required)
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { GeoGenApiClient } from "./api-client.js";
15
+ import { registerTools } from "./tools.js";
16
+ function main() {
17
+ const apiKey = process.env.GEOGEN_API_KEY;
18
+ const baseUrl = process.env.GEOGEN_BASE_URL;
19
+ if (!apiKey) {
20
+ console.error("Error: GEOGEN_API_KEY environment variable is required.");
21
+ console.error("Set it to your GeoGen workspace API key.");
22
+ process.exit(1);
23
+ }
24
+ if (!baseUrl) {
25
+ console.error("Error: GEOGEN_BASE_URL environment variable is required.");
26
+ console.error("Set it to your Convex site URL (e.g. https://your-app.convex.site).");
27
+ process.exit(1);
28
+ }
29
+ // Create the API client
30
+ const client = new GeoGenApiClient({ baseUrl, apiKey });
31
+ // Create the MCP server
32
+ const server = new McpServer({
33
+ name: "geogen",
34
+ version: "1.0.0",
35
+ });
36
+ // Register all tools
37
+ registerTools(server, client);
38
+ // Connect via stdio transport and start
39
+ const transport = new StdioServerTransport();
40
+ server.connect(transport).then(() => {
41
+ console.error("GeoGen MCP Server running on stdio");
42
+ }).catch((error) => {
43
+ console.error("Failed to start GeoGen MCP Server:", error);
44
+ process.exit(1);
45
+ });
46
+ }
47
+ main();
package/build/tools.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * GeoGen MCP Tool Definitions
3
+ * Registers all tools on the MCP server, mapping to the /v1 REST API.
4
+ */
5
+ import { z } from "zod";
6
+ /** Helper: wrap API responses as MCP text content */
7
+ function jsonContent(data) {
8
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
9
+ }
10
+ /** Helper: wrap errors as MCP text content */
11
+ function errorContent(err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ return {
14
+ content: [{ type: "text", text: `Error: ${message}` }],
15
+ isError: true,
16
+ };
17
+ }
18
+ // Reusable schema fragments
19
+ const periodSchema = z
20
+ .enum(["7d", "14d", "30d"])
21
+ .optional()
22
+ .describe("Time period: 7d, 14d, or 30d (default: 30d)");
23
+ const startDateSchema = z
24
+ .string()
25
+ .optional()
26
+ .describe("Start date (ISO format, e.g. 2025-01-01). Use with endDate instead of period.");
27
+ const endDateSchema = z
28
+ .string()
29
+ .optional()
30
+ .describe("End date (ISO format, e.g. 2025-01-31). Use with startDate instead of period.");
31
+ const entityIdSchema = z.string().describe("The entity ID to query");
32
+ const modelsSchema = z
33
+ .string()
34
+ .optional()
35
+ .describe("Comma-separated model UUIDs to filter by");
36
+ const tagsSchema = z
37
+ .string()
38
+ .optional()
39
+ .describe("Comma-separated tag IDs to filter by");
40
+ const promptIdsSchema = z
41
+ .string()
42
+ .optional()
43
+ .describe("Comma-separated prompt IDs to filter by");
44
+ const limitSchema = z
45
+ .number()
46
+ .int()
47
+ .positive()
48
+ .optional()
49
+ .describe("Max number of results to return");
50
+ const offsetSchema = z
51
+ .number()
52
+ .int()
53
+ .min(0)
54
+ .optional()
55
+ .describe("Number of results to skip (for pagination)");
56
+ export function registerTools(server, client) {
57
+ // ────────────────────────────────────────────────────────────
58
+ // 1. GET ENTITIES
59
+ // ────────────────────────────────────────────────────────────
60
+ server.tool("get_entities", "List all tracked websites/entities in your GeoGen workspace", {}, async () => {
61
+ try {
62
+ return jsonContent(await client.getEntities());
63
+ }
64
+ catch (err) {
65
+ return errorContent(err);
66
+ }
67
+ });
68
+ // ────────────────────────────────────────────────────────────
69
+ // 2. CREATE ENTITY
70
+ // ────────────────────────────────────────────────────────────
71
+ server.tool("create_entity", "Create a new tracked entity (website/brand) with optional AI-generated prompts. This consumes credits.", {
72
+ name: z.string().describe("Display name for the entity (e.g. 'My Company')"),
73
+ domain: z.string().describe("Domain to track (e.g. 'example.com')"),
74
+ description: z.string().optional().describe("Description of the entity for context"),
75
+ language: z.string().optional().describe("Language code (e.g. 'en', 'fr')"),
76
+ geolocation: z.string().optional().describe("Geolocation code"),
77
+ models: z.array(z.string()).optional().describe("Array of model UUIDs to track against"),
78
+ generatePrompts: z
79
+ .boolean()
80
+ .optional()
81
+ .describe("Auto-generate tracking prompts with AI (costs credits, default: true)"),
82
+ startTracking: z
83
+ .boolean()
84
+ .optional()
85
+ .describe("Start tracking immediately (default: true)"),
86
+ }, async (args) => {
87
+ try {
88
+ return jsonContent(await client.createEntity(args));
89
+ }
90
+ catch (err) {
91
+ return errorContent(err);
92
+ }
93
+ });
94
+ // ────────────────────────────────────────────────────────────
95
+ // 3. GET WORKSPACE
96
+ // ────────────────────────────────────────────────────────────
97
+ server.tool("get_workspace", "Get workspace usage, subscription, limits, and credit balance", {}, async () => {
98
+ try {
99
+ return jsonContent(await client.getWorkspace());
100
+ }
101
+ catch (err) {
102
+ return errorContent(err);
103
+ }
104
+ });
105
+ // ────────────────────────────────────────────────────────────
106
+ // 4. GET WORKSPACE MEMBERS
107
+ // ────────────────────────────────────────────────────────────
108
+ server.tool("get_workspace_members", "List all team members in the workspace", {}, async () => {
109
+ try {
110
+ return jsonContent(await client.getWorkspaceMembers());
111
+ }
112
+ catch (err) {
113
+ return errorContent(err);
114
+ }
115
+ });
116
+ // ────────────────────────────────────────────────────────────
117
+ // 5. GET WORKSPACE TAGS
118
+ // ────────────────────────────────────────────────────────────
119
+ server.tool("get_workspace_tags", "List all tags in the workspace (used to filter prompts)", {}, async () => {
120
+ try {
121
+ return jsonContent(await client.getWorkspaceTags());
122
+ }
123
+ catch (err) {
124
+ return errorContent(err);
125
+ }
126
+ });
127
+ // ────────────────────────────────────────────────────────────
128
+ // 6. GET MODELS
129
+ // ────────────────────────────────────────────────────────────
130
+ server.tool("get_models", "List all available LLM models that can be tracked (ChatGPT, Claude, Perplexity, etc.)", {}, async () => {
131
+ try {
132
+ return jsonContent(await client.getModels());
133
+ }
134
+ catch (err) {
135
+ return errorContent(err);
136
+ }
137
+ });
138
+ // ────────────────────────────────────────────────────────────
139
+ // 7. GET ENTITY PROMPTS
140
+ // ────────────────────────────────────────────────────────────
141
+ server.tool("get_entity_prompts", "Get all tracking prompts for a specific entity, with stats (visibility, mention rate, sentiment)", {
142
+ entityId: entityIdSchema,
143
+ period: periodSchema,
144
+ startDate: startDateSchema,
145
+ endDate: endDateSchema,
146
+ tags: tagsSchema,
147
+ }, async (args) => {
148
+ try {
149
+ return jsonContent(await client.getEntityPrompts(args));
150
+ }
151
+ catch (err) {
152
+ return errorContent(err);
153
+ }
154
+ });
155
+ // ────────────────────────────────────────────────────────────
156
+ // 8. ADD PROMPTS
157
+ // ────────────────────────────────────────────────────────────
158
+ server.tool("add_prompts", "Add one or more tracking prompts to an entity. Single: provide 'prompt'. Bulk: provide 'prompts' array.", {
159
+ entityId: entityIdSchema,
160
+ prompt: z.string().optional().describe("Single prompt text to add"),
161
+ prompts: z
162
+ .array(z.object({
163
+ prompt: z.string().describe("Prompt text"),
164
+ language: z.string().optional().describe("Language code"),
165
+ geolocation: z.string().optional().describe("Geolocation code"),
166
+ }))
167
+ .optional()
168
+ .describe("Array of prompts for bulk upload"),
169
+ language: z.string().optional().describe("Language for single prompt"),
170
+ geolocation: z.string().optional().describe("Geolocation for single prompt"),
171
+ }, async (args) => {
172
+ try {
173
+ return jsonContent(await client.addPrompts(args));
174
+ }
175
+ catch (err) {
176
+ return errorContent(err);
177
+ }
178
+ });
179
+ // ────────────────────────────────────────────────────────────
180
+ // 9. DELETE PROMPT
181
+ // ────────────────────────────────────────────────────────────
182
+ server.tool("delete_prompt", "Delete a tracking prompt by its ID", {
183
+ promptId: z.string().describe("The prompt ID to delete"),
184
+ }, async ({ promptId }) => {
185
+ try {
186
+ return jsonContent(await client.deletePrompt(promptId));
187
+ }
188
+ catch (err) {
189
+ return errorContent(err);
190
+ }
191
+ });
192
+ // ────────────────────────────────────────────────────────────
193
+ // 10. GET RESPONSES
194
+ // ────────────────────────────────────────────────────────────
195
+ server.tool("get_responses", "Get LLM responses for an entity with mention status. Filter by period, models, tags, or mention status.", {
196
+ entityId: entityIdSchema,
197
+ period: periodSchema,
198
+ startDate: startDateSchema,
199
+ endDate: endDateSchema,
200
+ models: modelsSchema,
201
+ tags: tagsSchema,
202
+ promptIds: promptIdsSchema,
203
+ mentionStatus: z
204
+ .enum(["all", "mentioned", "not-mentioned"])
205
+ .optional()
206
+ .describe("Filter by mention status"),
207
+ limit: limitSchema,
208
+ offset: offsetSchema,
209
+ }, async (args) => {
210
+ try {
211
+ return jsonContent(await client.getResponses({
212
+ ...args,
213
+ limit: args.limit?.toString(),
214
+ offset: args.offset?.toString(),
215
+ }));
216
+ }
217
+ catch (err) {
218
+ return errorContent(err);
219
+ }
220
+ });
221
+ // ────────────────────────────────────────────────────────────
222
+ // 11. GET RESPONSE DETAILS
223
+ // ────────────────────────────────────────────────────────────
224
+ server.tool("get_response_details", "Get detailed data for a single LLM response, including mentions, citations, and query fanouts", {
225
+ responseId: z.string().describe("The response ID"),
226
+ entityId: entityIdSchema,
227
+ }, async (args) => {
228
+ try {
229
+ return jsonContent(await client.getResponseDetails(args));
230
+ }
231
+ catch (err) {
232
+ return errorContent(err);
233
+ }
234
+ });
235
+ // ────────────────────────────────────────────────────────────
236
+ // 12. GET CITATIONS
237
+ // ────────────────────────────────────────────────────────────
238
+ server.tool("get_citations", "Get top cited domains for an entity — shows which websites LLMs reference when responding about this entity", {
239
+ entityId: entityIdSchema,
240
+ period: periodSchema,
241
+ startDate: startDateSchema,
242
+ endDate: endDateSchema,
243
+ models: modelsSchema,
244
+ tags: tagsSchema,
245
+ promptIds: promptIdsSchema,
246
+ limit: limitSchema,
247
+ offset: offsetSchema,
248
+ }, async (args) => {
249
+ try {
250
+ return jsonContent(await client.getCitations({
251
+ ...args,
252
+ limit: args.limit?.toString(),
253
+ offset: args.offset?.toString(),
254
+ }));
255
+ }
256
+ catch (err) {
257
+ return errorContent(err);
258
+ }
259
+ });
260
+ // ────────────────────────────────────────────────────────────
261
+ // 13. GET CITATIONS TREND
262
+ // ────────────────────────────────────────────────────────────
263
+ server.tool("get_citations_trend", "Get daily citation trend for top cited domains over time — shows how citation counts change day by day", {
264
+ entityId: entityIdSchema,
265
+ period: periodSchema,
266
+ startDate: startDateSchema,
267
+ endDate: endDateSchema,
268
+ models: modelsSchema,
269
+ tags: tagsSchema,
270
+ promptIds: promptIdsSchema,
271
+ topN: z
272
+ .number()
273
+ .int()
274
+ .positive()
275
+ .optional()
276
+ .describe("Number of top cited domains to include in the trend (default: all)"),
277
+ }, async (args) => {
278
+ try {
279
+ return jsonContent(await client.getCitationsTrend({
280
+ ...args,
281
+ topN: args.topN?.toString(),
282
+ }));
283
+ }
284
+ catch (err) {
285
+ return errorContent(err);
286
+ }
287
+ });
288
+ // ────────────────────────────────────────────────────────────
289
+ // 14. GET CITATION DETAILS
290
+ // ────────────────────────────────────────────────────────────
291
+ server.tool("get_citation_details", "Get detailed URLs for a specific cited domain — shows exactly which pages LLMs are linking to", {
292
+ entityId: entityIdSchema,
293
+ citedEntityUuid: z.string().describe("UUID of the cited domain to drill into"),
294
+ period: periodSchema,
295
+ startDate: startDateSchema,
296
+ endDate: endDateSchema,
297
+ models: modelsSchema,
298
+ limit: limitSchema,
299
+ offset: offsetSchema,
300
+ }, async (args) => {
301
+ try {
302
+ return jsonContent(await client.getCitationDetails({
303
+ ...args,
304
+ limit: args.limit?.toString(),
305
+ offset: args.offset?.toString(),
306
+ }));
307
+ }
308
+ catch (err) {
309
+ return errorContent(err);
310
+ }
311
+ });
312
+ // ────────────────────────────────────────────────────────────
313
+ // 15. GET COMPETITORS
314
+ // ────────────────────────────────────────────────────────────
315
+ server.tool("get_competitors", "Get competitor visibility leaderboard — see how your entity ranks against competitors in LLM mentions", {
316
+ entityId: entityIdSchema,
317
+ period: periodSchema,
318
+ startDate: startDateSchema,
319
+ endDate: endDateSchema,
320
+ models: modelsSchema,
321
+ competitorsOnly: z
322
+ .boolean()
323
+ .optional()
324
+ .describe("If true, only show competitors (exclude your entity)"),
325
+ limit: limitSchema,
326
+ offset: offsetSchema,
327
+ }, async (args) => {
328
+ try {
329
+ return jsonContent(await client.getCompetitors({
330
+ ...args,
331
+ competitorsOnly: args.competitorsOnly?.toString(),
332
+ limit: args.limit?.toString(),
333
+ offset: args.offset?.toString(),
334
+ }));
335
+ }
336
+ catch (err) {
337
+ return errorContent(err);
338
+ }
339
+ });
340
+ // ────────────────────────────────────────────────────────────
341
+ // 16. GET VISIBILITY TREND
342
+ // ────────────────────────────────────────────────────────────
343
+ server.tool("get_visibility_trend", "Get visibility trend over time — daily breakdown of mention rate, total responses, and visibility percentage", {
344
+ entityId: entityIdSchema,
345
+ period: periodSchema,
346
+ startDate: startDateSchema,
347
+ endDate: endDateSchema,
348
+ models: modelsSchema,
349
+ tags: tagsSchema,
350
+ promptIds: promptIdsSchema,
351
+ }, async (args) => {
352
+ try {
353
+ return jsonContent(await client.getVisibilityTrend(args));
354
+ }
355
+ catch (err) {
356
+ return errorContent(err);
357
+ }
358
+ });
359
+ // ────────────────────────────────────────────────────────────
360
+ // 17. GET SENTIMENT TREND
361
+ // ────────────────────────────────────────────────────────────
362
+ server.tool("get_sentiment_trend", "Get sentiment trend over time — daily breakdown of average sentiment score and mention count", {
363
+ entityId: entityIdSchema,
364
+ period: periodSchema,
365
+ startDate: startDateSchema,
366
+ endDate: endDateSchema,
367
+ models: modelsSchema,
368
+ tags: tagsSchema,
369
+ }, async (args) => {
370
+ try {
371
+ return jsonContent(await client.getSentimentTrend(args));
372
+ }
373
+ catch (err) {
374
+ return errorContent(err);
375
+ }
376
+ });
377
+ // ────────────────────────────────────────────────────────────
378
+ // 18. GET QUERY FANOUTS
379
+ // ────────────────────────────────────────────────────────────
380
+ server.tool("get_query_fanouts", "Get web search queries that LLMs performed while generating responses — shows what LLMs search for when answering about your entity", {
381
+ entityId: entityIdSchema,
382
+ period: periodSchema,
383
+ startDate: startDateSchema,
384
+ endDate: endDateSchema,
385
+ models: modelsSchema,
386
+ tags: tagsSchema,
387
+ limit: limitSchema,
388
+ offset: offsetSchema,
389
+ search: z.string().optional().describe("Full-text search filter on query text"),
390
+ }, async (args) => {
391
+ try {
392
+ return jsonContent(await client.getQueryFanouts({
393
+ ...args,
394
+ limit: args.limit?.toString(),
395
+ offset: args.offset?.toString(),
396
+ }));
397
+ }
398
+ catch (err) {
399
+ return errorContent(err);
400
+ }
401
+ });
402
+ }