@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/src/tools.ts ADDED
@@ -0,0 +1,520 @@
1
+ /**
2
+ * GeoGen MCP Tool Definitions
3
+ * Registers all tools on the MCP server, mapping to the /v1 REST API.
4
+ */
5
+
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { z } from "zod";
8
+ import { GeoGenApiClient } from "./api-client.js";
9
+
10
+ /** Helper: wrap API responses as MCP text content */
11
+ function jsonContent(data: unknown) {
12
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
13
+ }
14
+
15
+ /** Helper: wrap errors as MCP text content */
16
+ function errorContent(err: unknown) {
17
+ const message = err instanceof Error ? err.message : String(err);
18
+ return {
19
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
20
+ isError: true,
21
+ };
22
+ }
23
+
24
+ // Reusable schema fragments
25
+ const periodSchema = z
26
+ .enum(["7d", "14d", "30d"])
27
+ .optional()
28
+ .describe("Time period: 7d, 14d, or 30d (default: 30d)");
29
+
30
+ const startDateSchema = z
31
+ .string()
32
+ .optional()
33
+ .describe("Start date (ISO format, e.g. 2025-01-01). Use with endDate instead of period.");
34
+
35
+ const endDateSchema = z
36
+ .string()
37
+ .optional()
38
+ .describe("End date (ISO format, e.g. 2025-01-31). Use with startDate instead of period.");
39
+
40
+ const entityIdSchema = z.string().describe("The entity ID to query");
41
+
42
+ const modelsSchema = z
43
+ .string()
44
+ .optional()
45
+ .describe("Comma-separated model UUIDs to filter by");
46
+
47
+ const tagsSchema = z
48
+ .string()
49
+ .optional()
50
+ .describe("Comma-separated tag IDs to filter by");
51
+
52
+ const promptIdsSchema = z
53
+ .string()
54
+ .optional()
55
+ .describe("Comma-separated prompt IDs to filter by");
56
+
57
+ const limitSchema = z
58
+ .number()
59
+ .int()
60
+ .positive()
61
+ .optional()
62
+ .describe("Max number of results to return");
63
+
64
+ const offsetSchema = z
65
+ .number()
66
+ .int()
67
+ .min(0)
68
+ .optional()
69
+ .describe("Number of results to skip (for pagination)");
70
+
71
+ export function registerTools(server: McpServer, client: GeoGenApiClient) {
72
+ // ────────────────────────────────────────────────────────────
73
+ // 1. GET ENTITIES
74
+ // ────────────────────────────────────────────────────────────
75
+ server.tool(
76
+ "get_entities",
77
+ "List all tracked websites/entities in your GeoGen workspace",
78
+ {},
79
+ async () => {
80
+ try {
81
+ return jsonContent(await client.getEntities());
82
+ } catch (err) {
83
+ return errorContent(err);
84
+ }
85
+ }
86
+ );
87
+
88
+ // ────────────────────────────────────────────────────────────
89
+ // 2. CREATE ENTITY
90
+ // ────────────────────────────────────────────────────────────
91
+ server.tool(
92
+ "create_entity",
93
+ "Create a new tracked entity (website/brand) with optional AI-generated prompts. This consumes credits.",
94
+ {
95
+ name: z.string().describe("Display name for the entity (e.g. 'My Company')"),
96
+ domain: z.string().describe("Domain to track (e.g. 'example.com')"),
97
+ description: z.string().optional().describe("Description of the entity for context"),
98
+ language: z.string().optional().describe("Language code (e.g. 'en', 'fr')"),
99
+ geolocation: z.string().optional().describe("Geolocation code"),
100
+ models: z.array(z.string()).optional().describe("Array of model UUIDs to track against"),
101
+ generatePrompts: z
102
+ .boolean()
103
+ .optional()
104
+ .describe("Auto-generate tracking prompts with AI (costs credits, default: true)"),
105
+ startTracking: z
106
+ .boolean()
107
+ .optional()
108
+ .describe("Start tracking immediately (default: true)"),
109
+ },
110
+ async (args) => {
111
+ try {
112
+ return jsonContent(await client.createEntity(args));
113
+ } catch (err) {
114
+ return errorContent(err);
115
+ }
116
+ }
117
+ );
118
+
119
+ // ────────────────────────────────────────────────────────────
120
+ // 3. GET WORKSPACE
121
+ // ────────────────────────────────────────────────────────────
122
+ server.tool(
123
+ "get_workspace",
124
+ "Get workspace usage, subscription, limits, and credit balance",
125
+ {},
126
+ async () => {
127
+ try {
128
+ return jsonContent(await client.getWorkspace());
129
+ } catch (err) {
130
+ return errorContent(err);
131
+ }
132
+ }
133
+ );
134
+
135
+ // ────────────────────────────────────────────────────────────
136
+ // 4. GET WORKSPACE MEMBERS
137
+ // ────────────────────────────────────────────────────────────
138
+ server.tool(
139
+ "get_workspace_members",
140
+ "List all team members in the workspace",
141
+ {},
142
+ async () => {
143
+ try {
144
+ return jsonContent(await client.getWorkspaceMembers());
145
+ } catch (err) {
146
+ return errorContent(err);
147
+ }
148
+ }
149
+ );
150
+
151
+ // ────────────────────────────────────────────────────────────
152
+ // 5. GET WORKSPACE TAGS
153
+ // ────────────────────────────────────────────────────────────
154
+ server.tool(
155
+ "get_workspace_tags",
156
+ "List all tags in the workspace (used to filter prompts)",
157
+ {},
158
+ async () => {
159
+ try {
160
+ return jsonContent(await client.getWorkspaceTags());
161
+ } catch (err) {
162
+ return errorContent(err);
163
+ }
164
+ }
165
+ );
166
+
167
+ // ────────────────────────────────────────────────────────────
168
+ // 6. GET MODELS
169
+ // ────────────────────────────────────────────────────────────
170
+ server.tool(
171
+ "get_models",
172
+ "List all available LLM models that can be tracked (ChatGPT, Claude, Perplexity, etc.)",
173
+ {},
174
+ async () => {
175
+ try {
176
+ return jsonContent(await client.getModels());
177
+ } catch (err) {
178
+ return errorContent(err);
179
+ }
180
+ }
181
+ );
182
+
183
+ // ────────────────────────────────────────────────────────────
184
+ // 7. GET ENTITY PROMPTS
185
+ // ────────────────────────────────────────────────────────────
186
+ server.tool(
187
+ "get_entity_prompts",
188
+ "Get all tracking prompts for a specific entity, with stats (visibility, mention rate, sentiment)",
189
+ {
190
+ entityId: entityIdSchema,
191
+ period: periodSchema,
192
+ startDate: startDateSchema,
193
+ endDate: endDateSchema,
194
+ tags: tagsSchema,
195
+ },
196
+ async (args) => {
197
+ try {
198
+ return jsonContent(await client.getEntityPrompts(args));
199
+ } catch (err) {
200
+ return errorContent(err);
201
+ }
202
+ }
203
+ );
204
+
205
+ // ────────────────────────────────────────────────────────────
206
+ // 8. ADD PROMPTS
207
+ // ────────────────────────────────────────────────────────────
208
+ server.tool(
209
+ "add_prompts",
210
+ "Add one or more tracking prompts to an entity. Single: provide 'prompt'. Bulk: provide 'prompts' array.",
211
+ {
212
+ entityId: entityIdSchema,
213
+ prompt: z.string().optional().describe("Single prompt text to add"),
214
+ prompts: z
215
+ .array(
216
+ z.object({
217
+ prompt: z.string().describe("Prompt text"),
218
+ language: z.string().optional().describe("Language code"),
219
+ geolocation: z.string().optional().describe("Geolocation code"),
220
+ })
221
+ )
222
+ .optional()
223
+ .describe("Array of prompts for bulk upload"),
224
+ language: z.string().optional().describe("Language for single prompt"),
225
+ geolocation: z.string().optional().describe("Geolocation for single prompt"),
226
+ },
227
+ async (args) => {
228
+ try {
229
+ return jsonContent(await client.addPrompts(args));
230
+ } catch (err) {
231
+ return errorContent(err);
232
+ }
233
+ }
234
+ );
235
+
236
+ // ────────────────────────────────────────────────────────────
237
+ // 9. DELETE PROMPT
238
+ // ────────────────────────────────────────────────────────────
239
+ server.tool(
240
+ "delete_prompt",
241
+ "Delete a tracking prompt by its ID",
242
+ {
243
+ promptId: z.string().describe("The prompt ID to delete"),
244
+ },
245
+ async ({ promptId }) => {
246
+ try {
247
+ return jsonContent(await client.deletePrompt(promptId));
248
+ } catch (err) {
249
+ return errorContent(err);
250
+ }
251
+ }
252
+ );
253
+
254
+ // ────────────────────────────────────────────────────────────
255
+ // 10. GET RESPONSES
256
+ // ────────────────────────────────────────────────────────────
257
+ server.tool(
258
+ "get_responses",
259
+ "Get LLM responses for an entity with mention status. Filter by period, models, tags, or mention status.",
260
+ {
261
+ entityId: entityIdSchema,
262
+ period: periodSchema,
263
+ startDate: startDateSchema,
264
+ endDate: endDateSchema,
265
+ models: modelsSchema,
266
+ tags: tagsSchema,
267
+ promptIds: promptIdsSchema,
268
+ mentionStatus: z
269
+ .enum(["all", "mentioned", "not-mentioned"])
270
+ .optional()
271
+ .describe("Filter by mention status"),
272
+ limit: limitSchema,
273
+ offset: offsetSchema,
274
+ },
275
+ async (args) => {
276
+ try {
277
+ return jsonContent(
278
+ await client.getResponses({
279
+ ...args,
280
+ limit: args.limit?.toString(),
281
+ offset: args.offset?.toString(),
282
+ })
283
+ );
284
+ } catch (err) {
285
+ return errorContent(err);
286
+ }
287
+ }
288
+ );
289
+
290
+ // ────────────────────────────────────────────────────────────
291
+ // 11. GET RESPONSE DETAILS
292
+ // ────────────────────────────────────────────────────────────
293
+ server.tool(
294
+ "get_response_details",
295
+ "Get detailed data for a single LLM response, including mentions, citations, and query fanouts",
296
+ {
297
+ responseId: z.string().describe("The response ID"),
298
+ entityId: entityIdSchema,
299
+ },
300
+ async (args) => {
301
+ try {
302
+ return jsonContent(await client.getResponseDetails(args));
303
+ } catch (err) {
304
+ return errorContent(err);
305
+ }
306
+ }
307
+ );
308
+
309
+ // ────────────────────────────────────────────────────────────
310
+ // 12. GET CITATIONS
311
+ // ────────────────────────────────────────────────────────────
312
+ server.tool(
313
+ "get_citations",
314
+ "Get top cited domains for an entity — shows which websites LLMs reference when responding about this entity",
315
+ {
316
+ entityId: entityIdSchema,
317
+ period: periodSchema,
318
+ startDate: startDateSchema,
319
+ endDate: endDateSchema,
320
+ models: modelsSchema,
321
+ tags: tagsSchema,
322
+ promptIds: promptIdsSchema,
323
+ limit: limitSchema,
324
+ offset: offsetSchema,
325
+ },
326
+ async (args) => {
327
+ try {
328
+ return jsonContent(
329
+ await client.getCitations({
330
+ ...args,
331
+ limit: args.limit?.toString(),
332
+ offset: args.offset?.toString(),
333
+ })
334
+ );
335
+ } catch (err) {
336
+ return errorContent(err);
337
+ }
338
+ }
339
+ );
340
+
341
+ // ────────────────────────────────────────────────────────────
342
+ // 13. GET CITATIONS TREND
343
+ // ────────────────────────────────────────────────────────────
344
+ server.tool(
345
+ "get_citations_trend",
346
+ "Get daily citation trend for top cited domains over time — shows how citation counts change day by day",
347
+ {
348
+ entityId: entityIdSchema,
349
+ period: periodSchema,
350
+ startDate: startDateSchema,
351
+ endDate: endDateSchema,
352
+ models: modelsSchema,
353
+ tags: tagsSchema,
354
+ promptIds: promptIdsSchema,
355
+ topN: z
356
+ .number()
357
+ .int()
358
+ .positive()
359
+ .optional()
360
+ .describe("Number of top cited domains to include in the trend (default: all)"),
361
+ },
362
+ async (args) => {
363
+ try {
364
+ return jsonContent(
365
+ await client.getCitationsTrend({
366
+ ...args,
367
+ topN: args.topN?.toString(),
368
+ })
369
+ );
370
+ } catch (err) {
371
+ return errorContent(err);
372
+ }
373
+ }
374
+ );
375
+
376
+ // ────────────────────────────────────────────────────────────
377
+ // 14. GET CITATION DETAILS
378
+ // ────────────────────────────────────────────────────────────
379
+ server.tool(
380
+ "get_citation_details",
381
+ "Get detailed URLs for a specific cited domain — shows exactly which pages LLMs are linking to",
382
+ {
383
+ entityId: entityIdSchema,
384
+ citedEntityUuid: z.string().describe("UUID of the cited domain to drill into"),
385
+ period: periodSchema,
386
+ startDate: startDateSchema,
387
+ endDate: endDateSchema,
388
+ models: modelsSchema,
389
+ limit: limitSchema,
390
+ offset: offsetSchema,
391
+ },
392
+ async (args) => {
393
+ try {
394
+ return jsonContent(
395
+ await client.getCitationDetails({
396
+ ...args,
397
+ limit: args.limit?.toString(),
398
+ offset: args.offset?.toString(),
399
+ })
400
+ );
401
+ } catch (err) {
402
+ return errorContent(err);
403
+ }
404
+ }
405
+ );
406
+
407
+ // ────────────────────────────────────────────────────────────
408
+ // 15. GET COMPETITORS
409
+ // ────────────────────────────────────────────────────────────
410
+ server.tool(
411
+ "get_competitors",
412
+ "Get competitor visibility leaderboard — see how your entity ranks against competitors in LLM mentions",
413
+ {
414
+ entityId: entityIdSchema,
415
+ period: periodSchema,
416
+ startDate: startDateSchema,
417
+ endDate: endDateSchema,
418
+ models: modelsSchema,
419
+ competitorsOnly: z
420
+ .boolean()
421
+ .optional()
422
+ .describe("If true, only show competitors (exclude your entity)"),
423
+ limit: limitSchema,
424
+ offset: offsetSchema,
425
+ },
426
+ async (args) => {
427
+ try {
428
+ return jsonContent(
429
+ await client.getCompetitors({
430
+ ...args,
431
+ competitorsOnly: args.competitorsOnly?.toString(),
432
+ limit: args.limit?.toString(),
433
+ offset: args.offset?.toString(),
434
+ })
435
+ );
436
+ } catch (err) {
437
+ return errorContent(err);
438
+ }
439
+ }
440
+ );
441
+
442
+ // ────────────────────────────────────────────────────────────
443
+ // 16. GET VISIBILITY TREND
444
+ // ────────────────────────────────────────────────────────────
445
+ server.tool(
446
+ "get_visibility_trend",
447
+ "Get visibility trend over time — daily breakdown of mention rate, total responses, and visibility percentage",
448
+ {
449
+ entityId: entityIdSchema,
450
+ period: periodSchema,
451
+ startDate: startDateSchema,
452
+ endDate: endDateSchema,
453
+ models: modelsSchema,
454
+ tags: tagsSchema,
455
+ promptIds: promptIdsSchema,
456
+ },
457
+ async (args) => {
458
+ try {
459
+ return jsonContent(await client.getVisibilityTrend(args));
460
+ } catch (err) {
461
+ return errorContent(err);
462
+ }
463
+ }
464
+ );
465
+
466
+ // ────────────────────────────────────────────────────────────
467
+ // 17. GET SENTIMENT TREND
468
+ // ────────────────────────────────────────────────────────────
469
+ server.tool(
470
+ "get_sentiment_trend",
471
+ "Get sentiment trend over time — daily breakdown of average sentiment score and mention count",
472
+ {
473
+ entityId: entityIdSchema,
474
+ period: periodSchema,
475
+ startDate: startDateSchema,
476
+ endDate: endDateSchema,
477
+ models: modelsSchema,
478
+ tags: tagsSchema,
479
+ },
480
+ async (args) => {
481
+ try {
482
+ return jsonContent(await client.getSentimentTrend(args));
483
+ } catch (err) {
484
+ return errorContent(err);
485
+ }
486
+ }
487
+ );
488
+
489
+ // ────────────────────────────────────────────────────────────
490
+ // 18. GET QUERY FANOUTS
491
+ // ────────────────────────────────────────────────────────────
492
+ server.tool(
493
+ "get_query_fanouts",
494
+ "Get web search queries that LLMs performed while generating responses — shows what LLMs search for when answering about your entity",
495
+ {
496
+ entityId: entityIdSchema,
497
+ period: periodSchema,
498
+ startDate: startDateSchema,
499
+ endDate: endDateSchema,
500
+ models: modelsSchema,
501
+ tags: tagsSchema,
502
+ limit: limitSchema,
503
+ offset: offsetSchema,
504
+ search: z.string().optional().describe("Full-text search filter on query text"),
505
+ },
506
+ async (args) => {
507
+ try {
508
+ return jsonContent(
509
+ await client.getQueryFanouts({
510
+ ...args,
511
+ limit: args.limit?.toString(),
512
+ offset: args.offset?.toString(),
513
+ })
514
+ );
515
+ } catch (err) {
516
+ return errorContent(err);
517
+ }
518
+ }
519
+ );
520
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./build",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules"]
15
+ }