@geogenio/mcp-server 1.0.1 → 1.0.2
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/Dockerfile +8 -0
- package/build/api-client.js +21 -0
- package/build/index.js +121 -25
- package/build/tools.js +60 -1
- package/package.json +2 -1
- package/src/api-client.ts +29 -0
- package/src/index.ts +136 -29
- package/src/tools.ts +80 -1
package/Dockerfile
ADDED
package/build/api-client.js
CHANGED
|
@@ -54,6 +54,10 @@ export class GeoGenApiClient {
|
|
|
54
54
|
body: JSON.stringify(body),
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
async getTrackingStatus(params) {
|
|
58
|
+
const query = this.buildQuery(params);
|
|
59
|
+
return this.request(`/v1/entities/tracking-status${query}`);
|
|
60
|
+
}
|
|
57
61
|
async deletePrompt(promptId) {
|
|
58
62
|
const query = this.buildQuery({ promptId });
|
|
59
63
|
return this.request(`/v1/entities/deleteprompt${query}`, {
|
|
@@ -115,4 +119,21 @@ export class GeoGenApiClient {
|
|
|
115
119
|
const query = this.buildQuery(params);
|
|
116
120
|
return this.request(`/v1/query-fanouts${query}`);
|
|
117
121
|
}
|
|
122
|
+
// ── Actions (Actionables + Tasks) ─────────────────────────────
|
|
123
|
+
async getEntityActions(params) {
|
|
124
|
+
const query = this.buildQuery(params);
|
|
125
|
+
return this.request(`/v1/entities/actions${query}`);
|
|
126
|
+
}
|
|
127
|
+
async dismissActionable(actionableId) {
|
|
128
|
+
return this.request("/v1/entities/actions/dismiss", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: JSON.stringify({ actionableId }),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async updateTaskStatus(taskId, status) {
|
|
134
|
+
return this.request("/v1/entities/actions/update-status", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({ taskId, status }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
118
139
|
}
|
package/build/index.js
CHANGED
|
@@ -6,42 +6,138 @@
|
|
|
6
6
|
* for AI assistants (Claude Desktop, Cursor, Windsurf, Claude Code, etc.)
|
|
7
7
|
*
|
|
8
8
|
* Configuration via environment variables:
|
|
9
|
-
* GEOGEN_API_KEY - Your GeoGen workspace API key (required)
|
|
10
|
-
* GEOGEN_BASE_URL - GeoGen API URL (
|
|
9
|
+
* GEOGEN_API_KEY - Your GeoGen workspace API key (required for stdio mode)
|
|
10
|
+
* GEOGEN_BASE_URL - GeoGen API URL (default: https://api.geogen.io)
|
|
11
|
+
*
|
|
12
|
+
* Transport modes:
|
|
13
|
+
* --http - Start as HTTP server with SSE transport (for OpenAI Agent Builder, remote clients)
|
|
14
|
+
* --port <number> - Port for HTTP mode (default: 3100)
|
|
15
|
+
* (default) - Stdio transport (for Claude Desktop, Cursor, etc.)
|
|
16
|
+
*
|
|
17
|
+
* In HTTP mode, clients pass their API key via the Authorization header on the
|
|
18
|
+
* initial GET /sse request. No server-side API key is needed.
|
|
11
19
|
*/
|
|
12
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
14
23
|
import { GeoGenApiClient } from "./api-client.js";
|
|
15
24
|
import { registerTools } from "./tools.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 https://api.geogen.io");
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
// Create the API client
|
|
25
|
+
import http from "node:http";
|
|
26
|
+
const DEFAULT_BASE_URL = "https://api.geogen.io";
|
|
27
|
+
function createServer(apiKey, baseUrl) {
|
|
30
28
|
const client = new GeoGenApiClient({ baseUrl, apiKey });
|
|
31
|
-
// Create the MCP server
|
|
32
29
|
const server = new McpServer({
|
|
33
30
|
name: "geogen",
|
|
34
31
|
version: "1.0.0",
|
|
35
32
|
});
|
|
36
|
-
// Register all tools
|
|
37
33
|
registerTools(server, client);
|
|
38
|
-
|
|
34
|
+
return server;
|
|
35
|
+
}
|
|
36
|
+
function parseArgs() {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const httpMode = args.includes("--http");
|
|
39
|
+
const portIdx = args.indexOf("--port");
|
|
40
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 3100;
|
|
41
|
+
return { httpMode, port };
|
|
42
|
+
}
|
|
43
|
+
/** Extract Bearer token from Authorization header */
|
|
44
|
+
function extractApiKey(req) {
|
|
45
|
+
const auth = req.headers.authorization;
|
|
46
|
+
if (!auth)
|
|
47
|
+
return null;
|
|
48
|
+
if (auth.startsWith("Bearer "))
|
|
49
|
+
return auth.slice(7);
|
|
50
|
+
return auth;
|
|
51
|
+
}
|
|
52
|
+
async function startStdio(apiKey, baseUrl) {
|
|
53
|
+
const server = createServer(apiKey, baseUrl);
|
|
39
54
|
const transport = new StdioServerTransport();
|
|
40
|
-
server.connect(transport)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
await server.connect(transport);
|
|
56
|
+
console.error("GeoGen MCP Server running on stdio");
|
|
57
|
+
}
|
|
58
|
+
async function startHttp(baseUrl, port) {
|
|
59
|
+
// Track active SSE transports by session ID
|
|
60
|
+
const transports = new Map();
|
|
61
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
62
|
+
// CORS headers for remote clients
|
|
63
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
64
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
65
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
66
|
+
if (req.method === "OPTIONS") {
|
|
67
|
+
res.writeHead(204);
|
|
68
|
+
res.end();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
72
|
+
// GET /sse — establish SSE connection (client sends API key here)
|
|
73
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
74
|
+
const apiKey = extractApiKey(req);
|
|
75
|
+
if (!apiKey) {
|
|
76
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({ error: "Missing Authorization header. Pass your GeoGen API key as: Authorization: Bearer <key>" }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const server = createServer(apiKey, baseUrl);
|
|
81
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
82
|
+
transports.set(transport.sessionId, transport);
|
|
83
|
+
transport.onclose = () => {
|
|
84
|
+
transports.delete(transport.sessionId);
|
|
85
|
+
};
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// POST /messages — receive messages from client
|
|
90
|
+
if (req.method === "POST" && url.pathname === "/messages") {
|
|
91
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
92
|
+
if (!sessionId) {
|
|
93
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
94
|
+
res.end(JSON.stringify({ error: "Missing sessionId" }));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const transport = transports.get(sessionId);
|
|
98
|
+
if (!transport) {
|
|
99
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
100
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await transport.handlePostMessage(req, res);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Health check
|
|
107
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
108
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
109
|
+
res.end(JSON.stringify({ status: "ok", name: "geogen-mcp-server", transport: "sse" }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 404 for anything else
|
|
113
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
114
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
115
|
+
});
|
|
116
|
+
httpServer.listen(port, () => {
|
|
117
|
+
console.error(`GeoGen MCP Server running on http://localhost:${port}`);
|
|
118
|
+
console.error(` SSE endpoint: http://localhost:${port}/sse`);
|
|
119
|
+
console.error(` Messages: http://localhost:${port}/messages`);
|
|
45
120
|
});
|
|
46
121
|
}
|
|
47
|
-
main()
|
|
122
|
+
async function main() {
|
|
123
|
+
const { httpMode, port } = parseArgs();
|
|
124
|
+
const baseUrl = process.env.GEOGEN_BASE_URL || DEFAULT_BASE_URL;
|
|
125
|
+
if (httpMode) {
|
|
126
|
+
// HTTP mode: no API key needed on server — clients pass their own
|
|
127
|
+
await startHttp(baseUrl, port);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Stdio mode: API key from env (local usage)
|
|
131
|
+
const apiKey = process.env.GEOGEN_API_KEY;
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
console.error("Error: GEOGEN_API_KEY environment variable is required.");
|
|
134
|
+
console.error("Set it to your GeoGen workspace API key.");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
await startStdio(apiKey, baseUrl);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
main().catch((error) => {
|
|
141
|
+
console.error("Failed to start GeoGen MCP Server:", error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|
package/build/tools.js
CHANGED
|
@@ -190,7 +190,20 @@ export function registerTools(server, client) {
|
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
192
|
// ────────────────────────────────────────────────────────────
|
|
193
|
-
// 10. GET
|
|
193
|
+
// 10. GET TRACKING STATUS
|
|
194
|
+
// ────────────────────────────────────────────────────────────
|
|
195
|
+
server.tool("get_tracking_status", "Get the mention check status for an entity — when the last check completed, when the next is scheduled, and whether one is currently running", {
|
|
196
|
+
entityId: entityIdSchema,
|
|
197
|
+
}, async (args) => {
|
|
198
|
+
try {
|
|
199
|
+
return jsonContent(await client.getTrackingStatus(args));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return errorContent(err);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// ────────────────────────────────────────────────────────────
|
|
206
|
+
// 11. GET RESPONSES
|
|
194
207
|
// ────────────────────────────────────────────────────────────
|
|
195
208
|
server.tool("get_responses", "Get LLM responses for an entity with mention status. Filter by period, models, tags, or mention status.", {
|
|
196
209
|
entityId: entityIdSchema,
|
|
@@ -399,4 +412,50 @@ export function registerTools(server, client) {
|
|
|
399
412
|
return errorContent(err);
|
|
400
413
|
}
|
|
401
414
|
});
|
|
415
|
+
// ────────────────────────────────────────────────────────────
|
|
416
|
+
// 19. GET ENTITY ACTIONS (Actionables + Tasks)
|
|
417
|
+
// ────────────────────────────────────────────────────────────
|
|
418
|
+
server.tool("get_entity_actions", "Get AI-generated actionable recommendations and their associated tasks for an entity. Actionables are SEO improvement suggestions; tasks are actionables that have been moved to the kanban board for tracking.", {
|
|
419
|
+
entityId: entityIdSchema,
|
|
420
|
+
status: z
|
|
421
|
+
.enum(["active", "dismissed", "in_task"])
|
|
422
|
+
.optional()
|
|
423
|
+
.describe("Filter actionables by status (default: all)"),
|
|
424
|
+
}, async (args) => {
|
|
425
|
+
try {
|
|
426
|
+
return jsonContent(await client.getEntityActions(args));
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
return errorContent(err);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
// ────────────────────────────────────────────────────────────
|
|
433
|
+
// 20. DISMISS ACTIONABLE
|
|
434
|
+
// ────────────────────────────────────────────────────────────
|
|
435
|
+
server.tool("dismiss_actionable", "Dismiss an actionable recommendation so it no longer appears in the active feed. Dismissed actionables free up slots for new ones to be generated on the next mention check.", {
|
|
436
|
+
actionableId: z.string().describe("The actionable ID to dismiss"),
|
|
437
|
+
}, async (args) => {
|
|
438
|
+
try {
|
|
439
|
+
return jsonContent(await client.dismissActionable(args.actionableId));
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
return errorContent(err);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// ────────────────────────────────────────────────────────────
|
|
446
|
+
// 21. UPDATE TASK STATUS
|
|
447
|
+
// ────────────────────────────────────────────────────────────
|
|
448
|
+
server.tool("update_task_status", "Update the status of a task on the kanban board. Move tasks through the workflow: not_started → in_progress → in_review → done.", {
|
|
449
|
+
taskId: z.string().describe("The task ID to update"),
|
|
450
|
+
status: z
|
|
451
|
+
.enum(["not_started", "in_progress", "in_review", "done"])
|
|
452
|
+
.describe("New status for the task"),
|
|
453
|
+
}, async (args) => {
|
|
454
|
+
try {
|
|
455
|
+
return jsonContent(await client.updateTaskStatus(args.taskId, args.status));
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
return errorContent(err);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
402
461
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geogenio/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for GeoGen LLM SEO tracking platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node build/index.js",
|
|
12
|
+
"start:http": "node build/index.js --http",
|
|
12
13
|
"dev": "tsc --watch"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
package/src/api-client.ts
CHANGED
|
@@ -96,6 +96,11 @@ export class GeoGenApiClient {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
async getTrackingStatus(params: { entityId: string }): Promise<any> {
|
|
100
|
+
const query = this.buildQuery(params);
|
|
101
|
+
return this.request(`/v1/entities/tracking-status${query}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
async deletePrompt(promptId: string): Promise<any> {
|
|
100
105
|
const query = this.buildQuery({ promptId });
|
|
101
106
|
return this.request(`/v1/entities/deleteprompt${query}`, {
|
|
@@ -253,4 +258,28 @@ export class GeoGenApiClient {
|
|
|
253
258
|
const query = this.buildQuery(params);
|
|
254
259
|
return this.request(`/v1/query-fanouts${query}`);
|
|
255
260
|
}
|
|
261
|
+
|
|
262
|
+
// ── Actions (Actionables + Tasks) ─────────────────────────────
|
|
263
|
+
|
|
264
|
+
async getEntityActions(params: {
|
|
265
|
+
entityId: string;
|
|
266
|
+
status?: string;
|
|
267
|
+
}): Promise<any> {
|
|
268
|
+
const query = this.buildQuery(params);
|
|
269
|
+
return this.request(`/v1/entities/actions${query}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async dismissActionable(actionableId: string): Promise<any> {
|
|
273
|
+
return this.request("/v1/entities/actions/dismiss", {
|
|
274
|
+
method: "POST",
|
|
275
|
+
body: JSON.stringify({ actionableId }),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async updateTaskStatus(taskId: string, status: string): Promise<any> {
|
|
280
|
+
return this.request("/v1/entities/actions/update-status", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: JSON.stringify({ taskId, status }),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
256
285
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,51 +7,158 @@
|
|
|
7
7
|
* for AI assistants (Claude Desktop, Cursor, Windsurf, Claude Code, etc.)
|
|
8
8
|
*
|
|
9
9
|
* Configuration via environment variables:
|
|
10
|
-
* GEOGEN_API_KEY - Your GeoGen workspace API key (required)
|
|
11
|
-
* GEOGEN_BASE_URL - GeoGen API URL (
|
|
10
|
+
* GEOGEN_API_KEY - Your GeoGen workspace API key (required for stdio mode)
|
|
11
|
+
* GEOGEN_BASE_URL - GeoGen API URL (default: https://api.geogen.io)
|
|
12
|
+
*
|
|
13
|
+
* Transport modes:
|
|
14
|
+
* --http - Start as HTTP server with SSE transport (for OpenAI Agent Builder, remote clients)
|
|
15
|
+
* --port <number> - Port for HTTP mode (default: 3100)
|
|
16
|
+
* (default) - Stdio transport (for Claude Desktop, Cursor, etc.)
|
|
17
|
+
*
|
|
18
|
+
* In HTTP mode, clients pass their API key via the Authorization header on the
|
|
19
|
+
* initial GET /sse request. No server-side API key is needed.
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
23
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
16
25
|
import { GeoGenApiClient } from "./api-client.js";
|
|
17
26
|
import { registerTools } from "./tools.js";
|
|
27
|
+
import http from "node:http";
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
const apiKey = process.env.GEOGEN_API_KEY;
|
|
21
|
-
const baseUrl = process.env.GEOGEN_BASE_URL;
|
|
29
|
+
const DEFAULT_BASE_URL = "https://api.geogen.io";
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
console.error("Error: GEOGEN_API_KEY environment variable is required.");
|
|
25
|
-
console.error("Set it to your GeoGen workspace API key.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!baseUrl) {
|
|
30
|
-
console.error("Error: GEOGEN_BASE_URL environment variable is required.");
|
|
31
|
-
console.error("Set it to https://api.geogen.io");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Create the API client
|
|
31
|
+
function createServer(apiKey: string, baseUrl: string): McpServer {
|
|
36
32
|
const client = new GeoGenApiClient({ baseUrl, apiKey });
|
|
37
|
-
|
|
38
|
-
// Create the MCP server
|
|
39
33
|
const server = new McpServer({
|
|
40
34
|
name: "geogen",
|
|
41
35
|
version: "1.0.0",
|
|
42
36
|
});
|
|
43
|
-
|
|
44
|
-
// Register all tools
|
|
45
37
|
registerTools(server, client);
|
|
38
|
+
return server;
|
|
39
|
+
}
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
function parseArgs() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const httpMode = args.includes("--http");
|
|
44
|
+
const portIdx = args.indexOf("--port");
|
|
45
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 3100;
|
|
46
|
+
return { httpMode, port };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Extract Bearer token from Authorization header */
|
|
50
|
+
function extractApiKey(req: http.IncomingMessage): string | null {
|
|
51
|
+
const auth = req.headers.authorization;
|
|
52
|
+
if (!auth) return null;
|
|
53
|
+
if (auth.startsWith("Bearer ")) return auth.slice(7);
|
|
54
|
+
return auth;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function startStdio(apiKey: string, baseUrl: string) {
|
|
58
|
+
const server = createServer(apiKey, baseUrl);
|
|
48
59
|
const transport = new StdioServerTransport();
|
|
49
|
-
server.connect(transport)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
console.error("GeoGen MCP Server running on stdio");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function startHttp(baseUrl: string, port: number) {
|
|
65
|
+
// Track active SSE transports by session ID
|
|
66
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
67
|
+
|
|
68
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
69
|
+
// CORS headers for remote clients
|
|
70
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
71
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
72
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
73
|
+
|
|
74
|
+
if (req.method === "OPTIONS") {
|
|
75
|
+
res.writeHead(204);
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
81
|
+
|
|
82
|
+
// GET /sse — establish SSE connection (client sends API key here)
|
|
83
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
84
|
+
const apiKey = extractApiKey(req);
|
|
85
|
+
if (!apiKey) {
|
|
86
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
87
|
+
res.end(JSON.stringify({ error: "Missing Authorization header. Pass your GeoGen API key as: Authorization: Bearer <key>" }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const server = createServer(apiKey, baseUrl);
|
|
92
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
93
|
+
transports.set(transport.sessionId, transport);
|
|
94
|
+
|
|
95
|
+
transport.onclose = () => {
|
|
96
|
+
transports.delete(transport.sessionId);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await server.connect(transport);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// POST /messages — receive messages from client
|
|
104
|
+
if (req.method === "POST" && url.pathname === "/messages") {
|
|
105
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
106
|
+
if (!sessionId) {
|
|
107
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
108
|
+
res.end(JSON.stringify({ error: "Missing sessionId" }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const transport = transports.get(sessionId);
|
|
113
|
+
if (!transport) {
|
|
114
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
115
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await transport.handlePostMessage(req, res);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Health check
|
|
124
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
125
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify({ status: "ok", name: "geogen-mcp-server", transport: "sse" }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 404 for anything else
|
|
131
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
httpServer.listen(port, () => {
|
|
136
|
+
console.error(`GeoGen MCP Server running on http://localhost:${port}`);
|
|
137
|
+
console.error(` SSE endpoint: http://localhost:${port}/sse`);
|
|
138
|
+
console.error(` Messages: http://localhost:${port}/messages`);
|
|
54
139
|
});
|
|
55
140
|
}
|
|
56
141
|
|
|
57
|
-
main()
|
|
142
|
+
async function main() {
|
|
143
|
+
const { httpMode, port } = parseArgs();
|
|
144
|
+
const baseUrl = process.env.GEOGEN_BASE_URL || DEFAULT_BASE_URL;
|
|
145
|
+
|
|
146
|
+
if (httpMode) {
|
|
147
|
+
// HTTP mode: no API key needed on server — clients pass their own
|
|
148
|
+
await startHttp(baseUrl, port);
|
|
149
|
+
} else {
|
|
150
|
+
// Stdio mode: API key from env (local usage)
|
|
151
|
+
const apiKey = process.env.GEOGEN_API_KEY;
|
|
152
|
+
if (!apiKey) {
|
|
153
|
+
console.error("Error: GEOGEN_API_KEY environment variable is required.");
|
|
154
|
+
console.error("Set it to your GeoGen workspace API key.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
await startStdio(apiKey, baseUrl);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main().catch((error) => {
|
|
162
|
+
console.error("Failed to start GeoGen MCP Server:", error);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
package/src/tools.ts
CHANGED
|
@@ -252,7 +252,25 @@ export function registerTools(server: McpServer, client: GeoGenApiClient) {
|
|
|
252
252
|
);
|
|
253
253
|
|
|
254
254
|
// ────────────────────────────────────────────────────────────
|
|
255
|
-
// 10. GET
|
|
255
|
+
// 10. GET TRACKING STATUS
|
|
256
|
+
// ────────────────────────────────────────────────────────────
|
|
257
|
+
server.tool(
|
|
258
|
+
"get_tracking_status",
|
|
259
|
+
"Get the mention check status for an entity — when the last check completed, when the next is scheduled, and whether one is currently running",
|
|
260
|
+
{
|
|
261
|
+
entityId: entityIdSchema,
|
|
262
|
+
},
|
|
263
|
+
async (args) => {
|
|
264
|
+
try {
|
|
265
|
+
return jsonContent(await client.getTrackingStatus(args));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return errorContent(err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// ────────────────────────────────────────────────────────────
|
|
273
|
+
// 11. GET RESPONSES
|
|
256
274
|
// ────────────────────────────────────────────────────────────
|
|
257
275
|
server.tool(
|
|
258
276
|
"get_responses",
|
|
@@ -517,4 +535,65 @@ export function registerTools(server: McpServer, client: GeoGenApiClient) {
|
|
|
517
535
|
}
|
|
518
536
|
}
|
|
519
537
|
);
|
|
538
|
+
|
|
539
|
+
// ────────────────────────────────────────────────────────────
|
|
540
|
+
// 19. GET ENTITY ACTIONS (Actionables + Tasks)
|
|
541
|
+
// ────────────────────────────────────────────────────────────
|
|
542
|
+
server.tool(
|
|
543
|
+
"get_entity_actions",
|
|
544
|
+
"Get AI-generated actionable recommendations and their associated tasks for an entity. Actionables are SEO improvement suggestions; tasks are actionables that have been moved to the kanban board for tracking.",
|
|
545
|
+
{
|
|
546
|
+
entityId: entityIdSchema,
|
|
547
|
+
status: z
|
|
548
|
+
.enum(["active", "dismissed", "in_task"])
|
|
549
|
+
.optional()
|
|
550
|
+
.describe("Filter actionables by status (default: all)"),
|
|
551
|
+
},
|
|
552
|
+
async (args) => {
|
|
553
|
+
try {
|
|
554
|
+
return jsonContent(await client.getEntityActions(args));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
return errorContent(err);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// ────────────────────────────────────────────────────────────
|
|
562
|
+
// 20. DISMISS ACTIONABLE
|
|
563
|
+
// ────────────────────────────────────────────────────────────
|
|
564
|
+
server.tool(
|
|
565
|
+
"dismiss_actionable",
|
|
566
|
+
"Dismiss an actionable recommendation so it no longer appears in the active feed. Dismissed actionables free up slots for new ones to be generated on the next mention check.",
|
|
567
|
+
{
|
|
568
|
+
actionableId: z.string().describe("The actionable ID to dismiss"),
|
|
569
|
+
},
|
|
570
|
+
async (args) => {
|
|
571
|
+
try {
|
|
572
|
+
return jsonContent(await client.dismissActionable(args.actionableId));
|
|
573
|
+
} catch (err) {
|
|
574
|
+
return errorContent(err);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// ────────────────────────────────────────────────────────────
|
|
580
|
+
// 21. UPDATE TASK STATUS
|
|
581
|
+
// ────────────────────────────────────────────────────────────
|
|
582
|
+
server.tool(
|
|
583
|
+
"update_task_status",
|
|
584
|
+
"Update the status of a task on the kanban board. Move tasks through the workflow: not_started → in_progress → in_review → done.",
|
|
585
|
+
{
|
|
586
|
+
taskId: z.string().describe("The task ID to update"),
|
|
587
|
+
status: z
|
|
588
|
+
.enum(["not_started", "in_progress", "in_review", "done"])
|
|
589
|
+
.describe("New status for the task"),
|
|
590
|
+
},
|
|
591
|
+
async (args) => {
|
|
592
|
+
try {
|
|
593
|
+
return jsonContent(await client.updateTaskStatus(args.taskId, args.status));
|
|
594
|
+
} catch (err) {
|
|
595
|
+
return errorContent(err);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
);
|
|
520
599
|
}
|