@topgunbuild/mcp-server 0.9.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/LICENSE +97 -0
- package/README.md +199 -0
- package/dist/cli.js +1695 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +410 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1513 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1495 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1495 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { TopGunClient } from '@topgunbuild/client';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import pino from 'pino';
|
|
7
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
8
|
+
import { createServer } from 'http';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
|
|
11
|
+
// src/TopGunMCPServer.ts
|
|
12
|
+
var QueryArgsSchema = z.object({
|
|
13
|
+
map: z.string().describe("Name of the map to query (e.g., 'tasks', 'users', 'products')"),
|
|
14
|
+
filter: z.record(z.string(), z.unknown()).optional().describe('Filter criteria as key-value pairs. Example: { "status": "active", "priority": "high" }'),
|
|
15
|
+
sort: z.object({
|
|
16
|
+
field: z.string().describe("Field name to sort by"),
|
|
17
|
+
order: z.enum(["asc", "desc"]).describe("Sort order: ascending or descending")
|
|
18
|
+
}).optional().describe("Sort configuration"),
|
|
19
|
+
limit: z.number().optional().default(10).describe("Maximum number of results to return"),
|
|
20
|
+
offset: z.number().optional().default(0).describe("Number of results to skip (for pagination)")
|
|
21
|
+
});
|
|
22
|
+
var MutateArgsSchema = z.object({
|
|
23
|
+
map: z.string().describe("Name of the map to modify (e.g., 'tasks', 'users')"),
|
|
24
|
+
operation: z.enum(["set", "remove"]).describe('"set" creates or updates a record, "remove" deletes it'),
|
|
25
|
+
key: z.string().describe("Unique key for the record"),
|
|
26
|
+
data: z.record(z.string(), z.unknown()).optional().describe('Data to write (required for "set" operation)')
|
|
27
|
+
});
|
|
28
|
+
var SearchArgsSchema = z.object({
|
|
29
|
+
map: z.string().describe("Name of the map to search (e.g., 'articles', 'documents', 'tasks')"),
|
|
30
|
+
query: z.string().describe("Search query (keywords or phrases to find)"),
|
|
31
|
+
methods: z.array(z.enum(["exact", "fulltext", "range"])).optional().default(["exact", "fulltext"]).describe('Search methods to use. Default: ["exact", "fulltext"]'),
|
|
32
|
+
limit: z.number().optional().default(10).describe("Maximum number of results to return"),
|
|
33
|
+
minScore: z.number().optional().default(0).describe("Minimum relevance score (0-1) for results")
|
|
34
|
+
});
|
|
35
|
+
var SubscribeArgsSchema = z.object({
|
|
36
|
+
map: z.string().describe("Name of the map to watch (e.g., 'tasks', 'notifications')"),
|
|
37
|
+
filter: z.record(z.string(), z.unknown()).optional().describe("Filter criteria - only report changes matching these conditions"),
|
|
38
|
+
timeout: z.number().optional().default(60).describe("How long to watch for changes (in seconds)")
|
|
39
|
+
});
|
|
40
|
+
var SchemaArgsSchema = z.object({
|
|
41
|
+
map: z.string().describe("Name of the map to get schema for")
|
|
42
|
+
});
|
|
43
|
+
var StatsArgsSchema = z.object({
|
|
44
|
+
map: z.string().optional().describe("Specific map to get stats for (optional, returns all maps if not specified)")
|
|
45
|
+
});
|
|
46
|
+
var ExplainArgsSchema = z.object({
|
|
47
|
+
map: z.string().describe("Name of the map to query"),
|
|
48
|
+
filter: z.record(z.string(), z.unknown()).optional().describe("Filter criteria to analyze")
|
|
49
|
+
});
|
|
50
|
+
var ListMapsArgsSchema = z.object({});
|
|
51
|
+
var toolSchemas = {
|
|
52
|
+
query: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
map: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Name of the map to query (e.g., 'tasks', 'users', 'products')"
|
|
58
|
+
},
|
|
59
|
+
filter: {
|
|
60
|
+
type: "object",
|
|
61
|
+
description: 'Filter criteria as key-value pairs. Example: { "status": "active", "priority": "high" }',
|
|
62
|
+
additionalProperties: true
|
|
63
|
+
},
|
|
64
|
+
sort: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
field: { type: "string", description: "Field name to sort by" },
|
|
68
|
+
order: { type: "string", enum: ["asc", "desc"], description: "Sort order: ascending or descending" }
|
|
69
|
+
},
|
|
70
|
+
required: ["field", "order"],
|
|
71
|
+
description: "Sort configuration"
|
|
72
|
+
},
|
|
73
|
+
limit: { type: "number", description: "Maximum number of results to return", default: 10 },
|
|
74
|
+
offset: { type: "number", description: "Number of results to skip (for pagination)", default: 0 }
|
|
75
|
+
},
|
|
76
|
+
required: ["map"]
|
|
77
|
+
},
|
|
78
|
+
mutate: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
map: { type: "string", description: "Name of the map to modify (e.g., 'tasks', 'users')" },
|
|
82
|
+
operation: {
|
|
83
|
+
type: "string",
|
|
84
|
+
enum: ["set", "remove"],
|
|
85
|
+
description: '"set" creates or updates a record, "remove" deletes it'
|
|
86
|
+
},
|
|
87
|
+
key: { type: "string", description: "Unique key for the record" },
|
|
88
|
+
data: {
|
|
89
|
+
type: "object",
|
|
90
|
+
description: 'Data to write (required for "set" operation)',
|
|
91
|
+
additionalProperties: true
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
required: ["map", "operation", "key"]
|
|
95
|
+
},
|
|
96
|
+
search: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
map: { type: "string", description: "Name of the map to search (e.g., 'articles', 'documents', 'tasks')" },
|
|
100
|
+
query: { type: "string", description: "Search query (keywords or phrases to find)" },
|
|
101
|
+
methods: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: { type: "string", enum: ["exact", "fulltext", "range"] },
|
|
104
|
+
description: 'Search methods to use. Default: ["exact", "fulltext"]',
|
|
105
|
+
default: ["exact", "fulltext"]
|
|
106
|
+
},
|
|
107
|
+
limit: { type: "number", description: "Maximum number of results to return", default: 10 },
|
|
108
|
+
minScore: { type: "number", description: "Minimum relevance score (0-1) for results", default: 0 }
|
|
109
|
+
},
|
|
110
|
+
required: ["map", "query"]
|
|
111
|
+
},
|
|
112
|
+
subscribe: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
map: { type: "string", description: "Name of the map to watch (e.g., 'tasks', 'notifications')" },
|
|
116
|
+
filter: {
|
|
117
|
+
type: "object",
|
|
118
|
+
description: "Filter criteria - only report changes matching these conditions",
|
|
119
|
+
additionalProperties: true
|
|
120
|
+
},
|
|
121
|
+
timeout: { type: "number", description: "How long to watch for changes (in seconds)", default: 60 }
|
|
122
|
+
},
|
|
123
|
+
required: ["map"]
|
|
124
|
+
},
|
|
125
|
+
schema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
map: { type: "string", description: "Name of the map to get schema for" }
|
|
129
|
+
},
|
|
130
|
+
required: ["map"]
|
|
131
|
+
},
|
|
132
|
+
stats: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
map: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Specific map to get stats for (optional, returns all maps if not specified)"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
required: []
|
|
141
|
+
},
|
|
142
|
+
explain: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
map: { type: "string", description: "Name of the map to query" },
|
|
146
|
+
filter: {
|
|
147
|
+
type: "object",
|
|
148
|
+
description: "Filter criteria to analyze",
|
|
149
|
+
additionalProperties: true
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ["map"]
|
|
153
|
+
},
|
|
154
|
+
listMaps: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {},
|
|
157
|
+
required: []
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/tools/query.ts
|
|
162
|
+
var queryTool = {
|
|
163
|
+
name: "topgun_query",
|
|
164
|
+
description: "Query data from a TopGun map with filters and sorting. Use this to read data from the database. Supports filtering by field values and sorting by any field.",
|
|
165
|
+
inputSchema: toolSchemas.query
|
|
166
|
+
};
|
|
167
|
+
async function handleQuery(rawArgs, ctx) {
|
|
168
|
+
const parseResult = QueryArgsSchema.safeParse(rawArgs);
|
|
169
|
+
if (!parseResult.success) {
|
|
170
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
173
|
+
isError: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const { map, filter, sort, limit, offset } = parseResult.data;
|
|
177
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
isError: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const effectiveLimit = Math.min(limit ?? ctx.config.defaultLimit, ctx.config.maxLimit);
|
|
189
|
+
const effectiveOffset = offset ?? 0;
|
|
190
|
+
try {
|
|
191
|
+
const lwwMap = ctx.client.getMap(map);
|
|
192
|
+
const allEntries = [];
|
|
193
|
+
for (const [key, value] of lwwMap.entries()) {
|
|
194
|
+
if (value !== null && typeof value === "object") {
|
|
195
|
+
let matches = true;
|
|
196
|
+
if (filter) {
|
|
197
|
+
for (const [filterKey, filterValue] of Object.entries(filter)) {
|
|
198
|
+
if (value[filterKey] !== filterValue) {
|
|
199
|
+
matches = false;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (matches) {
|
|
205
|
+
allEntries.push({ key: String(key), value });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (sort?.field) {
|
|
210
|
+
allEntries.sort((a, b) => {
|
|
211
|
+
const aVal = a.value[sort.field];
|
|
212
|
+
const bVal = b.value[sort.field];
|
|
213
|
+
if (aVal === bVal) return 0;
|
|
214
|
+
if (aVal === void 0 || aVal === null) return 1;
|
|
215
|
+
if (bVal === void 0 || bVal === null) return -1;
|
|
216
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
217
|
+
return sort.order === "desc" ? -comparison : comparison;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const paginatedEntries = allEntries.slice(effectiveOffset, effectiveOffset + effectiveLimit);
|
|
221
|
+
if (paginatedEntries.length === 0) {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: `No results found in map '${map}'${filter ? " matching the filter" : ""}.`
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const formatted = paginatedEntries.map((entry, idx) => `${idx + 1 + effectiveOffset}. [${entry.key}]: ${JSON.stringify(entry.value, null, 2)}`).join("\n\n");
|
|
232
|
+
const totalInfo = allEntries.length > effectiveLimit ? `
|
|
233
|
+
|
|
234
|
+
(Showing ${effectiveOffset + 1}-${effectiveOffset + paginatedEntries.length} of ${allEntries.length} total)` : "";
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: `Found ${paginatedEntries.length} result(s) in map '${map}':
|
|
240
|
+
|
|
241
|
+
${formatted}${totalInfo}`
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: "text",
|
|
251
|
+
text: `Error querying map '${map}': ${message}`
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
isError: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/tools/mutate.ts
|
|
260
|
+
var mutateTool = {
|
|
261
|
+
name: "topgun_mutate",
|
|
262
|
+
description: 'Create, update, or delete data in a TopGun map. Use "set" operation to create or update a record. Use "remove" operation to delete a record.',
|
|
263
|
+
inputSchema: toolSchemas.mutate
|
|
264
|
+
};
|
|
265
|
+
async function handleMutate(rawArgs, ctx) {
|
|
266
|
+
const parseResult = MutateArgsSchema.safeParse(rawArgs);
|
|
267
|
+
if (!parseResult.success) {
|
|
268
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
271
|
+
isError: true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const { map, operation, key, data } = parseResult.data;
|
|
275
|
+
if (!ctx.config.enableMutations) {
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: "Error: Mutation operations are disabled on this MCP server."
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
isError: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
isError: true
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const lwwMap = ctx.client.getMap(map);
|
|
299
|
+
if (operation === "set") {
|
|
300
|
+
if (!data) {
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: 'Error: "data" is required for "set" operation.'
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
isError: true
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const recordData = {
|
|
312
|
+
...data,
|
|
313
|
+
_updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
314
|
+
};
|
|
315
|
+
const existingValue = lwwMap.get(key);
|
|
316
|
+
const isCreate = existingValue === void 0;
|
|
317
|
+
lwwMap.set(key, recordData);
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: "text",
|
|
322
|
+
text: isCreate ? `Successfully created record '${key}' in map '${map}':
|
|
323
|
+
${JSON.stringify(recordData, null, 2)}` : `Successfully updated record '${key}' in map '${map}':
|
|
324
|
+
${JSON.stringify(recordData, null, 2)}`
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
};
|
|
328
|
+
} else if (operation === "remove") {
|
|
329
|
+
const existingValue = lwwMap.get(key);
|
|
330
|
+
if (existingValue === void 0) {
|
|
331
|
+
return {
|
|
332
|
+
content: [
|
|
333
|
+
{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: `Warning: Record '${key}' does not exist in map '${map}'. No action taken.`
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
lwwMap.remove(key);
|
|
341
|
+
return {
|
|
342
|
+
content: [
|
|
343
|
+
{
|
|
344
|
+
type: "text",
|
|
345
|
+
text: `Successfully removed record '${key}' from map '${map}'.`
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: "text",
|
|
354
|
+
text: `Error: Invalid operation '${operation}'. Use 'set' or 'remove'.`
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
isError: true
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: "text",
|
|
365
|
+
text: `Error performing ${operation} on map '${map}': ${message}`
|
|
366
|
+
}
|
|
367
|
+
],
|
|
368
|
+
isError: true
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/tools/search.ts
|
|
374
|
+
var searchTool = {
|
|
375
|
+
name: "topgun_search",
|
|
376
|
+
description: "Perform hybrid search across a TopGun map using BM25 full-text search. Returns results ranked by relevance score. Use this when searching for text content or when the exact field values are unknown.",
|
|
377
|
+
inputSchema: toolSchemas.search
|
|
378
|
+
};
|
|
379
|
+
async function handleSearch(rawArgs, ctx) {
|
|
380
|
+
const parseResult = SearchArgsSchema.safeParse(rawArgs);
|
|
381
|
+
if (!parseResult.success) {
|
|
382
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
385
|
+
isError: true
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const args = parseResult.data;
|
|
389
|
+
const { map, query, limit, minScore } = args;
|
|
390
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
396
|
+
}
|
|
397
|
+
],
|
|
398
|
+
isError: true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const effectiveLimit = Math.min(limit ?? ctx.config.defaultLimit, ctx.config.maxLimit);
|
|
402
|
+
const effectiveMinScore = minScore ?? 0;
|
|
403
|
+
try {
|
|
404
|
+
const results = await ctx.client.search(map, query, {
|
|
405
|
+
limit: effectiveLimit,
|
|
406
|
+
minScore: effectiveMinScore
|
|
407
|
+
});
|
|
408
|
+
if (results.length === 0) {
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: `No results found in map '${map}' for query "${query}".`
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const formatted = results.map(
|
|
419
|
+
(result, idx) => `${idx + 1}. [Score: ${result.score.toFixed(3)}] [${result.key}]
|
|
420
|
+
Matched: ${result.matchedTerms.join(", ")}
|
|
421
|
+
Data: ${JSON.stringify(result.value, null, 2).split("\n").join("\n ")}`
|
|
422
|
+
).join("\n\n");
|
|
423
|
+
return {
|
|
424
|
+
content: [
|
|
425
|
+
{
|
|
426
|
+
type: "text",
|
|
427
|
+
text: `Found ${results.length} result(s) in map '${map}' for query "${query}":
|
|
428
|
+
|
|
429
|
+
${formatted}`
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
435
|
+
if (message.includes("not enabled") || message.includes("FTS")) {
|
|
436
|
+
return {
|
|
437
|
+
content: [
|
|
438
|
+
{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: `Full-text search is not enabled for map '${map}'. Use topgun_query instead for exact matching, or enable FTS on the server.`
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
isError: true
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: `Error searching map '${map}': ${message}`
|
|
451
|
+
}
|
|
452
|
+
],
|
|
453
|
+
isError: true
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/tools/subscribe.ts
|
|
459
|
+
var subscribeTool = {
|
|
460
|
+
name: "topgun_subscribe",
|
|
461
|
+
description: "Subscribe to real-time changes in a TopGun map. Returns changes that occur within the timeout period. Use this to watch for new or updated data.",
|
|
462
|
+
inputSchema: toolSchemas.subscribe
|
|
463
|
+
};
|
|
464
|
+
async function handleSubscribe(rawArgs, ctx) {
|
|
465
|
+
const parseResult = SubscribeArgsSchema.safeParse(rawArgs);
|
|
466
|
+
if (!parseResult.success) {
|
|
467
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
470
|
+
isError: true
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const args = parseResult.data;
|
|
474
|
+
const { map, filter, timeout } = args;
|
|
475
|
+
if (!ctx.config.enableSubscriptions) {
|
|
476
|
+
return {
|
|
477
|
+
content: [
|
|
478
|
+
{
|
|
479
|
+
type: "text",
|
|
480
|
+
text: "Error: Subscription operations are disabled on this MCP server."
|
|
481
|
+
}
|
|
482
|
+
],
|
|
483
|
+
isError: true
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
487
|
+
return {
|
|
488
|
+
content: [
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
492
|
+
}
|
|
493
|
+
],
|
|
494
|
+
isError: true
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const effectiveTimeout = Math.min(
|
|
498
|
+
timeout ?? ctx.config.subscriptionTimeoutSeconds,
|
|
499
|
+
ctx.config.subscriptionTimeoutSeconds
|
|
500
|
+
);
|
|
501
|
+
try {
|
|
502
|
+
const queryHandle = ctx.client.query(map, filter ?? {});
|
|
503
|
+
const changes = [];
|
|
504
|
+
let isInitialLoad = true;
|
|
505
|
+
const unsubscribe = queryHandle.subscribe((results) => {
|
|
506
|
+
if (isInitialLoad) {
|
|
507
|
+
isInitialLoad = false;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
for (const result of results) {
|
|
511
|
+
changes.push({
|
|
512
|
+
type: "update",
|
|
513
|
+
key: result._key ?? "unknown",
|
|
514
|
+
value: result,
|
|
515
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
await new Promise((resolve) => setTimeout(resolve, effectiveTimeout * 1e3));
|
|
520
|
+
unsubscribe();
|
|
521
|
+
if (changes.length === 0) {
|
|
522
|
+
return {
|
|
523
|
+
content: [
|
|
524
|
+
{
|
|
525
|
+
type: "text",
|
|
526
|
+
text: `No changes detected in map '${map}' during the ${effectiveTimeout} second watch period.`
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const formatted = changes.map(
|
|
532
|
+
(change, idx) => `${idx + 1}. [${change.timestamp}] ${change.type.toUpperCase()} - ${change.key}
|
|
533
|
+
` + (change.value ? ` ${JSON.stringify(change.value, null, 2).split("\n").join("\n ")}` : "")
|
|
534
|
+
).join("\n\n");
|
|
535
|
+
return {
|
|
536
|
+
content: [
|
|
537
|
+
{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: `Detected ${changes.length} change(s) in map '${map}' during ${effectiveTimeout} seconds:
|
|
540
|
+
|
|
541
|
+
${formatted}`
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
};
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
547
|
+
return {
|
|
548
|
+
content: [
|
|
549
|
+
{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: `Error subscribing to map '${map}': ${message}`
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
isError: true
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/tools/schema.ts
|
|
560
|
+
var schemaTool = {
|
|
561
|
+
name: "topgun_schema",
|
|
562
|
+
description: "Get schema information about a TopGun map. Returns inferred field types and indexes. Use this to understand the structure of data in a map.",
|
|
563
|
+
inputSchema: toolSchemas.schema
|
|
564
|
+
};
|
|
565
|
+
function inferType(value) {
|
|
566
|
+
if (value === null) return "null";
|
|
567
|
+
if (value === void 0) return "undefined";
|
|
568
|
+
if (Array.isArray(value)) {
|
|
569
|
+
if (value.length === 0) return "array";
|
|
570
|
+
const itemTypes = [...new Set(value.map((v) => inferType(v)))];
|
|
571
|
+
return `array<${itemTypes.join(" | ")}>`;
|
|
572
|
+
}
|
|
573
|
+
if (value instanceof Date) return "date";
|
|
574
|
+
if (typeof value === "object") return "object";
|
|
575
|
+
return typeof value;
|
|
576
|
+
}
|
|
577
|
+
function isTimestamp(value) {
|
|
578
|
+
if (typeof value === "string") {
|
|
579
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) return true;
|
|
580
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return true;
|
|
581
|
+
}
|
|
582
|
+
if (typeof value === "number") {
|
|
583
|
+
if (value > 9466848e5 && value < 41024448e5) return true;
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
function inferEnum(values) {
|
|
588
|
+
const uniqueValues = [...new Set(values.filter((v) => typeof v === "string"))];
|
|
589
|
+
if (uniqueValues.length >= 2 && uniqueValues.length <= 10) {
|
|
590
|
+
return uniqueValues;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
async function handleSchema(rawArgs, ctx) {
|
|
595
|
+
const parseResult = SchemaArgsSchema.safeParse(rawArgs);
|
|
596
|
+
if (!parseResult.success) {
|
|
597
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
600
|
+
isError: true
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const args = parseResult.data;
|
|
604
|
+
const { map } = args;
|
|
605
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
606
|
+
return {
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
isError: true
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const lwwMap = ctx.client.getMap(map);
|
|
618
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
619
|
+
const fieldValues = /* @__PURE__ */ new Map();
|
|
620
|
+
let recordCount = 0;
|
|
621
|
+
for (const [, value] of lwwMap.entries()) {
|
|
622
|
+
if (value !== null && typeof value === "object") {
|
|
623
|
+
recordCount++;
|
|
624
|
+
for (const [fieldName, fieldValue] of Object.entries(value)) {
|
|
625
|
+
if (!fieldTypes.has(fieldName)) {
|
|
626
|
+
fieldTypes.set(fieldName, /* @__PURE__ */ new Set());
|
|
627
|
+
}
|
|
628
|
+
let inferredType = inferType(fieldValue);
|
|
629
|
+
if (isTimestamp(fieldValue)) {
|
|
630
|
+
inferredType = "timestamp";
|
|
631
|
+
}
|
|
632
|
+
fieldTypes.get(fieldName).add(inferredType);
|
|
633
|
+
if (!fieldValues.has(fieldName)) {
|
|
634
|
+
fieldValues.set(fieldName, []);
|
|
635
|
+
}
|
|
636
|
+
fieldValues.get(fieldName).push(fieldValue);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (recordCount === 0) {
|
|
641
|
+
return {
|
|
642
|
+
content: [
|
|
643
|
+
{
|
|
644
|
+
type: "text",
|
|
645
|
+
text: `Map '${map}' is empty. No schema information available.
|
|
646
|
+
|
|
647
|
+
Tip: Add some data to the map to infer its schema.`
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const fields = {};
|
|
653
|
+
for (const [fieldName, types] of fieldTypes.entries()) {
|
|
654
|
+
const typeArray = [...types];
|
|
655
|
+
const values = fieldValues.get(fieldName) ?? [];
|
|
656
|
+
const enumValues = inferEnum(values);
|
|
657
|
+
if (enumValues && typeArray.length === 1 && typeArray[0] === "string") {
|
|
658
|
+
fields[fieldName] = `enum(${enumValues.join(", ")})`;
|
|
659
|
+
} else if (typeArray.length === 1) {
|
|
660
|
+
fields[fieldName] = typeArray[0];
|
|
661
|
+
} else {
|
|
662
|
+
fields[fieldName] = typeArray.join(" | ");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const schemaOutput = {
|
|
666
|
+
map,
|
|
667
|
+
recordCount,
|
|
668
|
+
fields,
|
|
669
|
+
// Note: Index information would come from server metadata in a full implementation
|
|
670
|
+
indexes: []
|
|
671
|
+
};
|
|
672
|
+
const fieldsFormatted = Object.entries(fields).map(([name, type]) => ` - ${name}: ${type}`).join("\n");
|
|
673
|
+
return {
|
|
674
|
+
content: [
|
|
675
|
+
{
|
|
676
|
+
type: "text",
|
|
677
|
+
text: `Schema for map '${map}':
|
|
678
|
+
|
|
679
|
+
Records: ${recordCount}
|
|
680
|
+
|
|
681
|
+
Fields:
|
|
682
|
+
${fieldsFormatted}
|
|
683
|
+
|
|
684
|
+
Raw schema:
|
|
685
|
+
${JSON.stringify(schemaOutput, null, 2)}`
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
} catch (error) {
|
|
690
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
691
|
+
return {
|
|
692
|
+
content: [
|
|
693
|
+
{
|
|
694
|
+
type: "text",
|
|
695
|
+
text: `Error getting schema for map '${map}': ${message}`
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
isError: true
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/tools/stats.ts
|
|
704
|
+
var statsTool = {
|
|
705
|
+
name: "topgun_stats",
|
|
706
|
+
description: "Get statistics about TopGun maps. Returns record counts, connection status, and sync state. Use this to understand the health and size of your data.",
|
|
707
|
+
inputSchema: toolSchemas.stats
|
|
708
|
+
};
|
|
709
|
+
async function handleStats(rawArgs, ctx) {
|
|
710
|
+
const parseResult = StatsArgsSchema.safeParse(rawArgs);
|
|
711
|
+
if (!parseResult.success) {
|
|
712
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
713
|
+
return {
|
|
714
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
715
|
+
isError: true
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const args = parseResult.data;
|
|
719
|
+
const { map } = args;
|
|
720
|
+
if (map && ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
721
|
+
return {
|
|
722
|
+
content: [
|
|
723
|
+
{
|
|
724
|
+
type: "text",
|
|
725
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
726
|
+
}
|
|
727
|
+
],
|
|
728
|
+
isError: true
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const stats = {
|
|
733
|
+
connection: {
|
|
734
|
+
state: ctx.client.getConnectionState(),
|
|
735
|
+
isCluster: ctx.client.isCluster(),
|
|
736
|
+
pendingOps: ctx.client.getPendingOpsCount(),
|
|
737
|
+
backpressurePaused: ctx.client.isBackpressurePaused()
|
|
738
|
+
},
|
|
739
|
+
maps: []
|
|
740
|
+
};
|
|
741
|
+
if (ctx.client.isCluster()) {
|
|
742
|
+
stats.cluster = {
|
|
743
|
+
nodes: ctx.client.getConnectedNodes(),
|
|
744
|
+
partitionMapVersion: ctx.client.getPartitionMapVersion(),
|
|
745
|
+
routingActive: ctx.client.isRoutingActive()
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (map) {
|
|
749
|
+
const lwwMap = ctx.client.getMap(map);
|
|
750
|
+
let recordCount = 0;
|
|
751
|
+
let tombstoneCount = 0;
|
|
752
|
+
for (const [, value] of lwwMap.entries()) {
|
|
753
|
+
if (value === null || value === void 0) {
|
|
754
|
+
tombstoneCount++;
|
|
755
|
+
} else {
|
|
756
|
+
recordCount++;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
stats.maps.push({
|
|
760
|
+
name: map,
|
|
761
|
+
recordCount,
|
|
762
|
+
tombstoneCount
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const connectionInfo = `Connection Status:
|
|
766
|
+
- State: ${stats.connection.state}
|
|
767
|
+
- Mode: ${stats.connection.isCluster ? "Cluster" : "Single Server"}
|
|
768
|
+
- Pending Operations: ${stats.connection.pendingOps}
|
|
769
|
+
- Backpressure Paused: ${stats.connection.backpressurePaused}`;
|
|
770
|
+
const clusterInfo = stats.cluster ? `
|
|
771
|
+
|
|
772
|
+
Cluster Info:
|
|
773
|
+
- Connected Nodes: ${stats.cluster.nodes.length > 0 ? stats.cluster.nodes.join(", ") : "none"}
|
|
774
|
+
- Partition Map Version: ${stats.cluster.partitionMapVersion}
|
|
775
|
+
- Routing Active: ${stats.cluster.routingActive}` : "";
|
|
776
|
+
const mapInfo = stats.maps.length > 0 ? `
|
|
777
|
+
|
|
778
|
+
Map Statistics:
|
|
779
|
+
` + stats.maps.map(
|
|
780
|
+
(m) => ` ${m.name}:
|
|
781
|
+
- Records: ${m.recordCount}
|
|
782
|
+
- Tombstones: ${m.tombstoneCount}`
|
|
783
|
+
).join("\n") : map ? `
|
|
784
|
+
|
|
785
|
+
Map '${map}' has no data yet.` : "";
|
|
786
|
+
return {
|
|
787
|
+
content: [
|
|
788
|
+
{
|
|
789
|
+
type: "text",
|
|
790
|
+
text: `TopGun Statistics:
|
|
791
|
+
|
|
792
|
+
${connectionInfo}${clusterInfo}${mapInfo}`
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
};
|
|
796
|
+
} catch (error) {
|
|
797
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
798
|
+
return {
|
|
799
|
+
content: [
|
|
800
|
+
{
|
|
801
|
+
type: "text",
|
|
802
|
+
text: `Error getting stats: ${message}`
|
|
803
|
+
}
|
|
804
|
+
],
|
|
805
|
+
isError: true
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/tools/explain.ts
|
|
811
|
+
var explainTool = {
|
|
812
|
+
name: "topgun_explain",
|
|
813
|
+
description: "Explain how a query would be executed against a TopGun map. Returns the query plan, estimated result count, and execution strategy. Use this to understand and optimize queries.",
|
|
814
|
+
inputSchema: toolSchemas.explain
|
|
815
|
+
};
|
|
816
|
+
async function handleExplain(rawArgs, ctx) {
|
|
817
|
+
const parseResult = ExplainArgsSchema.safeParse(rawArgs);
|
|
818
|
+
if (!parseResult.success) {
|
|
819
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
820
|
+
return {
|
|
821
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
822
|
+
isError: true
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
const args = parseResult.data;
|
|
826
|
+
const { map, filter } = args;
|
|
827
|
+
if (ctx.config.allowedMaps && !ctx.config.allowedMaps.includes(map)) {
|
|
828
|
+
return {
|
|
829
|
+
content: [
|
|
830
|
+
{
|
|
831
|
+
type: "text",
|
|
832
|
+
text: `Error: Access to map '${map}' is not allowed. Available maps: ${ctx.config.allowedMaps.join(", ")}`
|
|
833
|
+
}
|
|
834
|
+
],
|
|
835
|
+
isError: true
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const lwwMap = ctx.client.getMap(map);
|
|
840
|
+
let totalRecords = 0;
|
|
841
|
+
let matchingRecords = 0;
|
|
842
|
+
for (const [, value] of lwwMap.entries()) {
|
|
843
|
+
if (value !== null && typeof value === "object") {
|
|
844
|
+
totalRecords++;
|
|
845
|
+
if (filter) {
|
|
846
|
+
let matches = true;
|
|
847
|
+
for (const [filterKey, filterValue] of Object.entries(filter)) {
|
|
848
|
+
if (value[filterKey] !== filterValue) {
|
|
849
|
+
matches = false;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (matches) matchingRecords++;
|
|
854
|
+
} else {
|
|
855
|
+
matchingRecords++;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const plan = {
|
|
860
|
+
strategy: filter ? "FILTER_SCAN" : "FULL_SCAN",
|
|
861
|
+
steps: [],
|
|
862
|
+
estimatedResults: matchingRecords,
|
|
863
|
+
totalRecords,
|
|
864
|
+
selectivity: totalRecords > 0 ? matchingRecords / totalRecords : 0,
|
|
865
|
+
recommendations: []
|
|
866
|
+
};
|
|
867
|
+
plan.steps.push(`1. Scan map '${map}' (${totalRecords} records)`);
|
|
868
|
+
if (filter) {
|
|
869
|
+
const filterFields = Object.keys(filter);
|
|
870
|
+
plan.steps.push(`2. Apply filter on fields: ${filterFields.join(", ")}`);
|
|
871
|
+
plan.steps.push(`3. Return matching records (estimated: ${matchingRecords})`);
|
|
872
|
+
if (plan.selectivity < 0.1) {
|
|
873
|
+
plan.recommendations.push(
|
|
874
|
+
`Consider creating an index on ${filterFields.join(", ")} for better performance.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (totalRecords > 1e3 && plan.selectivity > 0.5) {
|
|
878
|
+
plan.recommendations.push(
|
|
879
|
+
`Query is not selective (${(plan.selectivity * 100).toFixed(1)}% of records match). Consider adding more filter criteria.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
plan.steps.push(`2. Return all records`);
|
|
884
|
+
if (totalRecords > 100) {
|
|
885
|
+
plan.recommendations.push(
|
|
886
|
+
`No filter applied. Consider adding filter criteria to reduce result size.`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (filter) {
|
|
891
|
+
const stringFilters = Object.entries(filter).filter(
|
|
892
|
+
([, v]) => typeof v === "string" && String(v).length > 3
|
|
893
|
+
);
|
|
894
|
+
if (stringFilters.length > 0) {
|
|
895
|
+
plan.recommendations.push(
|
|
896
|
+
`For text search on fields [${stringFilters.map(([k]) => k).join(", ")}], consider using topgun_search instead for better relevance ranking.`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const stepsFormatted = plan.steps.join("\n");
|
|
901
|
+
const recommendationsFormatted = plan.recommendations.length > 0 ? `
|
|
902
|
+
|
|
903
|
+
Recommendations:
|
|
904
|
+
${plan.recommendations.map((r) => ` - ${r}`).join("\n")}` : "";
|
|
905
|
+
return {
|
|
906
|
+
content: [
|
|
907
|
+
{
|
|
908
|
+
type: "text",
|
|
909
|
+
text: `Query Plan for map '${map}':
|
|
910
|
+
|
|
911
|
+
Strategy: ${plan.strategy}
|
|
912
|
+
|
|
913
|
+
Execution Steps:
|
|
914
|
+
${stepsFormatted}
|
|
915
|
+
|
|
916
|
+
Statistics:
|
|
917
|
+
- Total Records: ${plan.totalRecords}
|
|
918
|
+
- Estimated Results: ${plan.estimatedResults}
|
|
919
|
+
- Selectivity: ${(plan.selectivity * 100).toFixed(1)}%` + recommendationsFormatted
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
};
|
|
923
|
+
} catch (error) {
|
|
924
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
925
|
+
return {
|
|
926
|
+
content: [
|
|
927
|
+
{
|
|
928
|
+
type: "text",
|
|
929
|
+
text: `Error explaining query on map '${map}': ${message}`
|
|
930
|
+
}
|
|
931
|
+
],
|
|
932
|
+
isError: true
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/tools/listMaps.ts
|
|
938
|
+
var listMapsTool = {
|
|
939
|
+
name: "topgun_list_maps",
|
|
940
|
+
description: "List all available TopGun maps that can be queried. Returns the names of maps you have access to. Use this first to discover what data is available.",
|
|
941
|
+
inputSchema: toolSchemas.listMaps
|
|
942
|
+
};
|
|
943
|
+
async function handleListMaps(rawArgs, ctx) {
|
|
944
|
+
const parseResult = ListMapsArgsSchema.safeParse(rawArgs);
|
|
945
|
+
if (!parseResult.success) {
|
|
946
|
+
const errors = parseResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
947
|
+
return {
|
|
948
|
+
content: [{ type: "text", text: `Invalid arguments: ${errors}` }],
|
|
949
|
+
isError: true
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
if (ctx.config.allowedMaps && ctx.config.allowedMaps.length > 0) {
|
|
954
|
+
const mapList = ctx.config.allowedMaps.map((name) => ` - ${name}`).join("\n");
|
|
955
|
+
return {
|
|
956
|
+
content: [
|
|
957
|
+
{
|
|
958
|
+
type: "text",
|
|
959
|
+
text: `Available maps (${ctx.config.allowedMaps.length}):
|
|
960
|
+
${mapList}
|
|
961
|
+
|
|
962
|
+
Use topgun_schema to get field information for a specific map.
|
|
963
|
+
Use topgun_query to read data from a map.`
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
content: [
|
|
970
|
+
{
|
|
971
|
+
type: "text",
|
|
972
|
+
text: `This MCP server allows access to all maps (no restrictions configured).
|
|
973
|
+
|
|
974
|
+
To query a map, use topgun_query with the map name.
|
|
975
|
+
To get schema information, use topgun_schema.
|
|
976
|
+
To search, use topgun_search.
|
|
977
|
+
|
|
978
|
+
Common map patterns:
|
|
979
|
+
- 'users' - User accounts
|
|
980
|
+
- 'tasks' - Task items
|
|
981
|
+
- 'posts' - Blog posts or messages
|
|
982
|
+
- 'products' - E-commerce products
|
|
983
|
+
|
|
984
|
+
Tip: Ask the user what maps are available in their application.`
|
|
985
|
+
}
|
|
986
|
+
]
|
|
987
|
+
};
|
|
988
|
+
} catch (error) {
|
|
989
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
990
|
+
return {
|
|
991
|
+
content: [
|
|
992
|
+
{
|
|
993
|
+
type: "text",
|
|
994
|
+
text: `Error listing maps: ${message}`
|
|
995
|
+
}
|
|
996
|
+
],
|
|
997
|
+
isError: true
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/tools/index.ts
|
|
1003
|
+
var allTools = [
|
|
1004
|
+
listMapsTool,
|
|
1005
|
+
queryTool,
|
|
1006
|
+
mutateTool,
|
|
1007
|
+
searchTool,
|
|
1008
|
+
subscribeTool,
|
|
1009
|
+
schemaTool,
|
|
1010
|
+
statsTool,
|
|
1011
|
+
explainTool
|
|
1012
|
+
];
|
|
1013
|
+
var toolHandlers = {
|
|
1014
|
+
topgun_list_maps: handleListMaps,
|
|
1015
|
+
topgun_query: handleQuery,
|
|
1016
|
+
topgun_mutate: handleMutate,
|
|
1017
|
+
topgun_search: handleSearch,
|
|
1018
|
+
topgun_subscribe: handleSubscribe,
|
|
1019
|
+
topgun_schema: handleSchema,
|
|
1020
|
+
topgun_stats: handleStats,
|
|
1021
|
+
topgun_explain: handleExplain
|
|
1022
|
+
};
|
|
1023
|
+
function createLogger(options = {}) {
|
|
1024
|
+
const { debug = false, name = "topgun-mcp" } = options;
|
|
1025
|
+
return pino({
|
|
1026
|
+
name,
|
|
1027
|
+
level: debug ? "debug" : "info",
|
|
1028
|
+
// Always use stderr to not interfere with MCP stdio protocol
|
|
1029
|
+
transport: void 0
|
|
1030
|
+
}, pino.destination(2));
|
|
1031
|
+
}
|
|
1032
|
+
createLogger();
|
|
1033
|
+
|
|
1034
|
+
// src/TopGunMCPServer.ts
|
|
1035
|
+
var DEFAULT_CONFIG = {
|
|
1036
|
+
name: "topgun-mcp-server",
|
|
1037
|
+
version: "1.0.0",
|
|
1038
|
+
topgunUrl: "ws://localhost:8080",
|
|
1039
|
+
enableMutations: true,
|
|
1040
|
+
enableSubscriptions: true,
|
|
1041
|
+
defaultLimit: 10,
|
|
1042
|
+
maxLimit: 100,
|
|
1043
|
+
subscriptionTimeoutSeconds: 60,
|
|
1044
|
+
debug: false
|
|
1045
|
+
};
|
|
1046
|
+
var TopGunMCPServer = class {
|
|
1047
|
+
server;
|
|
1048
|
+
client;
|
|
1049
|
+
config;
|
|
1050
|
+
toolContext;
|
|
1051
|
+
logger;
|
|
1052
|
+
isStarted = false;
|
|
1053
|
+
externalClient = false;
|
|
1054
|
+
constructor(config = {}) {
|
|
1055
|
+
this.config = {
|
|
1056
|
+
name: config.name ?? DEFAULT_CONFIG.name,
|
|
1057
|
+
version: config.version ?? DEFAULT_CONFIG.version,
|
|
1058
|
+
topgunUrl: config.topgunUrl ?? DEFAULT_CONFIG.topgunUrl,
|
|
1059
|
+
authToken: config.authToken,
|
|
1060
|
+
allowedMaps: config.allowedMaps,
|
|
1061
|
+
enableMutations: config.enableMutations ?? DEFAULT_CONFIG.enableMutations,
|
|
1062
|
+
enableSubscriptions: config.enableSubscriptions ?? DEFAULT_CONFIG.enableSubscriptions,
|
|
1063
|
+
defaultLimit: config.defaultLimit ?? DEFAULT_CONFIG.defaultLimit,
|
|
1064
|
+
maxLimit: config.maxLimit ?? DEFAULT_CONFIG.maxLimit,
|
|
1065
|
+
subscriptionTimeoutSeconds: config.subscriptionTimeoutSeconds ?? DEFAULT_CONFIG.subscriptionTimeoutSeconds,
|
|
1066
|
+
debug: config.debug ?? DEFAULT_CONFIG.debug
|
|
1067
|
+
};
|
|
1068
|
+
if (config.client) {
|
|
1069
|
+
this.client = config.client;
|
|
1070
|
+
this.externalClient = true;
|
|
1071
|
+
} else {
|
|
1072
|
+
this.client = new TopGunClient({
|
|
1073
|
+
serverUrl: this.config.topgunUrl,
|
|
1074
|
+
storage: new InMemoryStorageAdapter()
|
|
1075
|
+
});
|
|
1076
|
+
if (this.config.authToken) {
|
|
1077
|
+
this.client.setAuthToken(this.config.authToken);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
this.toolContext = {
|
|
1081
|
+
client: this.client,
|
|
1082
|
+
config: this.config
|
|
1083
|
+
};
|
|
1084
|
+
this.logger = createLogger({
|
|
1085
|
+
debug: this.config.debug,
|
|
1086
|
+
name: this.config.name
|
|
1087
|
+
});
|
|
1088
|
+
this.server = new Server(
|
|
1089
|
+
{
|
|
1090
|
+
name: this.config.name,
|
|
1091
|
+
version: this.config.version
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
capabilities: {
|
|
1095
|
+
tools: {}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
this.registerHandlers();
|
|
1100
|
+
this.logger.info({
|
|
1101
|
+
topgunUrl: this.config.topgunUrl,
|
|
1102
|
+
allowedMaps: this.config.allowedMaps,
|
|
1103
|
+
enableMutations: this.config.enableMutations
|
|
1104
|
+
}, "TopGunMCPServer initialized");
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Register MCP protocol handlers
|
|
1108
|
+
*/
|
|
1109
|
+
registerHandlers() {
|
|
1110
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1111
|
+
let availableTools = [...allTools];
|
|
1112
|
+
if (!this.config.enableMutations) {
|
|
1113
|
+
availableTools = availableTools.filter((t) => t.name !== "topgun_mutate");
|
|
1114
|
+
}
|
|
1115
|
+
if (!this.config.enableSubscriptions) {
|
|
1116
|
+
availableTools = availableTools.filter((t) => t.name !== "topgun_subscribe");
|
|
1117
|
+
}
|
|
1118
|
+
this.logger.debug({ count: availableTools.length }, "tools/list called");
|
|
1119
|
+
return {
|
|
1120
|
+
tools: availableTools.map((tool) => ({
|
|
1121
|
+
name: tool.name,
|
|
1122
|
+
description: tool.description,
|
|
1123
|
+
inputSchema: tool.inputSchema
|
|
1124
|
+
}))
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1128
|
+
const { name, arguments: args } = request.params;
|
|
1129
|
+
this.logger.debug({ name, args }, "tools/call");
|
|
1130
|
+
const handler = toolHandlers[name];
|
|
1131
|
+
if (!handler) {
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: `Unknown tool: ${name}. Use tools/list to see available tools.`
|
|
1137
|
+
}
|
|
1138
|
+
],
|
|
1139
|
+
isError: true
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const result = await handler(args ?? {}, this.toolContext);
|
|
1144
|
+
this.logger.debug({ name, isError: result.isError }, "Tool result");
|
|
1145
|
+
return {
|
|
1146
|
+
content: result.content.map((c) => ({
|
|
1147
|
+
type: "text",
|
|
1148
|
+
text: c.text ?? ""
|
|
1149
|
+
})),
|
|
1150
|
+
isError: result.isError
|
|
1151
|
+
};
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1154
|
+
this.logger.error({ name, error: message }, "Tool error");
|
|
1155
|
+
return {
|
|
1156
|
+
content: [
|
|
1157
|
+
{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: `Error executing ${name}: ${message}`
|
|
1160
|
+
}
|
|
1161
|
+
],
|
|
1162
|
+
isError: true
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Start the MCP server with stdio transport
|
|
1169
|
+
*/
|
|
1170
|
+
async start() {
|
|
1171
|
+
if (this.isStarted) {
|
|
1172
|
+
throw new Error("Server is already started");
|
|
1173
|
+
}
|
|
1174
|
+
await this.client.start();
|
|
1175
|
+
const transport = new StdioServerTransport();
|
|
1176
|
+
await this.server.connect(transport);
|
|
1177
|
+
this.isStarted = true;
|
|
1178
|
+
this.logger.info("TopGun MCP Server started on stdio");
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Start the MCP server with a custom transport
|
|
1182
|
+
*/
|
|
1183
|
+
async startWithTransport(transport) {
|
|
1184
|
+
if (this.isStarted) {
|
|
1185
|
+
throw new Error("Server is already started");
|
|
1186
|
+
}
|
|
1187
|
+
await this.client.start();
|
|
1188
|
+
await this.server.connect(transport);
|
|
1189
|
+
this.isStarted = true;
|
|
1190
|
+
this.logger.info("TopGun MCP Server started with custom transport");
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Stop the server and cleanup resources
|
|
1194
|
+
*/
|
|
1195
|
+
async stop() {
|
|
1196
|
+
if (!this.isStarted) return;
|
|
1197
|
+
await this.server.close();
|
|
1198
|
+
if (!this.externalClient) {
|
|
1199
|
+
this.client.close();
|
|
1200
|
+
}
|
|
1201
|
+
this.isStarted = false;
|
|
1202
|
+
this.logger.info("TopGun MCP Server stopped");
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Execute a tool directly (for testing)
|
|
1206
|
+
*/
|
|
1207
|
+
async callTool(name, args) {
|
|
1208
|
+
const handler = toolHandlers[name];
|
|
1209
|
+
if (!handler) {
|
|
1210
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1211
|
+
}
|
|
1212
|
+
return handler(args, this.toolContext);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Get the underlying MCP server instance
|
|
1216
|
+
*/
|
|
1217
|
+
getServer() {
|
|
1218
|
+
return this.server;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Get the TopGun client instance
|
|
1222
|
+
*/
|
|
1223
|
+
getClient() {
|
|
1224
|
+
return this.client;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Get resolved configuration
|
|
1228
|
+
*/
|
|
1229
|
+
getConfig() {
|
|
1230
|
+
return { ...this.config };
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var InMemoryStorageAdapter = class {
|
|
1234
|
+
data = /* @__PURE__ */ new Map();
|
|
1235
|
+
meta = /* @__PURE__ */ new Map();
|
|
1236
|
+
opLog = [];
|
|
1237
|
+
opLogIdCounter = 0;
|
|
1238
|
+
async initialize(_name) {
|
|
1239
|
+
}
|
|
1240
|
+
async close() {
|
|
1241
|
+
this.data.clear();
|
|
1242
|
+
this.meta.clear();
|
|
1243
|
+
this.opLog = [];
|
|
1244
|
+
}
|
|
1245
|
+
async get(key) {
|
|
1246
|
+
return this.data.get(key);
|
|
1247
|
+
}
|
|
1248
|
+
async put(key, value) {
|
|
1249
|
+
this.data.set(key, value);
|
|
1250
|
+
}
|
|
1251
|
+
async remove(key) {
|
|
1252
|
+
this.data.delete(key);
|
|
1253
|
+
}
|
|
1254
|
+
async getAllKeys() {
|
|
1255
|
+
return Array.from(this.data.keys());
|
|
1256
|
+
}
|
|
1257
|
+
async getMeta(key) {
|
|
1258
|
+
return this.meta.get(key);
|
|
1259
|
+
}
|
|
1260
|
+
async setMeta(key, value) {
|
|
1261
|
+
this.meta.set(key, value);
|
|
1262
|
+
}
|
|
1263
|
+
async batchPut(entries) {
|
|
1264
|
+
for (const [key, value] of entries) {
|
|
1265
|
+
this.data.set(key, value);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async appendOpLog(entry) {
|
|
1269
|
+
const id = ++this.opLogIdCounter;
|
|
1270
|
+
this.opLog.push({ ...entry, id });
|
|
1271
|
+
return id;
|
|
1272
|
+
}
|
|
1273
|
+
async getPendingOps() {
|
|
1274
|
+
return this.opLog.filter((e) => e.synced === 0);
|
|
1275
|
+
}
|
|
1276
|
+
async markOpsSynced(lastId) {
|
|
1277
|
+
for (const op of this.opLog) {
|
|
1278
|
+
if (op.id !== void 0 && op.id <= lastId) {
|
|
1279
|
+
op.synced = 1;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
var DEFAULT_HTTP_CONFIG = {
|
|
1285
|
+
port: 3e3,
|
|
1286
|
+
host: "0.0.0.0",
|
|
1287
|
+
corsOrigins: ["*"],
|
|
1288
|
+
mcpPath: "/mcp",
|
|
1289
|
+
eventPath: "/mcp/events",
|
|
1290
|
+
debug: false
|
|
1291
|
+
};
|
|
1292
|
+
var HTTPTransport = class {
|
|
1293
|
+
config;
|
|
1294
|
+
httpServer;
|
|
1295
|
+
isRunning = false;
|
|
1296
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
1297
|
+
constructor(config = {}) {
|
|
1298
|
+
this.config = {
|
|
1299
|
+
port: config.port ?? DEFAULT_HTTP_CONFIG.port,
|
|
1300
|
+
host: config.host ?? DEFAULT_HTTP_CONFIG.host,
|
|
1301
|
+
corsOrigins: config.corsOrigins ?? DEFAULT_HTTP_CONFIG.corsOrigins,
|
|
1302
|
+
mcpPath: config.mcpPath ?? DEFAULT_HTTP_CONFIG.mcpPath,
|
|
1303
|
+
eventPath: config.eventPath ?? DEFAULT_HTTP_CONFIG.eventPath,
|
|
1304
|
+
debug: config.debug ?? DEFAULT_HTTP_CONFIG.debug
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Start HTTP server with MCP transport
|
|
1309
|
+
*/
|
|
1310
|
+
async start(mcpServer) {
|
|
1311
|
+
if (this.isRunning) {
|
|
1312
|
+
throw new Error("HTTP transport is already running");
|
|
1313
|
+
}
|
|
1314
|
+
this.httpServer = createServer((req, res) => {
|
|
1315
|
+
this.handleRequest(req, res, mcpServer);
|
|
1316
|
+
});
|
|
1317
|
+
return new Promise((resolve, reject) => {
|
|
1318
|
+
this.httpServer.on("error", reject);
|
|
1319
|
+
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
1320
|
+
this.isRunning = true;
|
|
1321
|
+
this.log(`HTTP transport listening on ${this.config.host}:${this.config.port}`);
|
|
1322
|
+
resolve();
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Stop HTTP server
|
|
1328
|
+
*/
|
|
1329
|
+
async stop() {
|
|
1330
|
+
if (!this.isRunning || !this.httpServer) return;
|
|
1331
|
+
for (const [sessionId, transport] of this.activeSessions) {
|
|
1332
|
+
try {
|
|
1333
|
+
await transport.close();
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
this.activeSessions.delete(sessionId);
|
|
1337
|
+
}
|
|
1338
|
+
return new Promise((resolve) => {
|
|
1339
|
+
this.httpServer.close(() => {
|
|
1340
|
+
this.isRunning = false;
|
|
1341
|
+
this.log("HTTP transport stopped");
|
|
1342
|
+
resolve();
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Handle incoming HTTP request
|
|
1348
|
+
*/
|
|
1349
|
+
async handleRequest(req, res, mcpServer) {
|
|
1350
|
+
this.setCorsHeaders(req, res);
|
|
1351
|
+
if (req.method === "OPTIONS") {
|
|
1352
|
+
res.writeHead(204);
|
|
1353
|
+
res.end();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
1357
|
+
const pathname = url.pathname;
|
|
1358
|
+
this.log(`${req.method} ${pathname}`);
|
|
1359
|
+
if (pathname === "/health") {
|
|
1360
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1361
|
+
res.end(JSON.stringify({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (pathname === this.config.mcpPath && req.method === "GET") {
|
|
1365
|
+
const config = mcpServer.getConfig();
|
|
1366
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1367
|
+
res.end(
|
|
1368
|
+
JSON.stringify({
|
|
1369
|
+
name: config.name,
|
|
1370
|
+
version: config.version,
|
|
1371
|
+
transport: "http+sse",
|
|
1372
|
+
mcpPath: this.config.mcpPath,
|
|
1373
|
+
eventPath: this.config.eventPath
|
|
1374
|
+
})
|
|
1375
|
+
);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (pathname === this.config.eventPath && req.method === "GET") {
|
|
1379
|
+
await this.handleSSEConnection(req, res, mcpServer);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (pathname === this.config.mcpPath && req.method === "POST") {
|
|
1383
|
+
await this.handleMCPRequest(req, res, mcpServer);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1387
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Handle SSE connection for real-time MCP
|
|
1391
|
+
*/
|
|
1392
|
+
async handleSSEConnection(_req, res, mcpServer) {
|
|
1393
|
+
const sessionId = randomUUID();
|
|
1394
|
+
this.log(`New SSE session: ${sessionId}`);
|
|
1395
|
+
const transport = new SSEServerTransport(this.config.mcpPath, res);
|
|
1396
|
+
this.activeSessions.set(sessionId, transport);
|
|
1397
|
+
try {
|
|
1398
|
+
await mcpServer.getServer().connect(transport);
|
|
1399
|
+
await new Promise((resolve) => {
|
|
1400
|
+
res.on("close", () => {
|
|
1401
|
+
this.log(`SSE session closed: ${sessionId}`);
|
|
1402
|
+
this.activeSessions.delete(sessionId);
|
|
1403
|
+
resolve();
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
this.log(`SSE session error: ${sessionId}`, error);
|
|
1408
|
+
this.activeSessions.delete(sessionId);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Handle stateless MCP POST request
|
|
1413
|
+
*/
|
|
1414
|
+
async handleMCPRequest(req, res, mcpServer) {
|
|
1415
|
+
try {
|
|
1416
|
+
const body = await this.readBody(req);
|
|
1417
|
+
const request = JSON.parse(body);
|
|
1418
|
+
this.log("MCP request", request);
|
|
1419
|
+
if (request.method === "tools/call") {
|
|
1420
|
+
const { name, arguments: args } = request.params || {};
|
|
1421
|
+
if (!name) {
|
|
1422
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1423
|
+
res.end(JSON.stringify({ error: "Missing tool name" }));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const result = await mcpServer.callTool(name, args);
|
|
1427
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1428
|
+
res.end(JSON.stringify({ result }));
|
|
1429
|
+
} else {
|
|
1430
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1431
|
+
res.end(
|
|
1432
|
+
JSON.stringify({
|
|
1433
|
+
error: "Unsupported method. Use SSE transport for full MCP support."
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1439
|
+
this.log("MCP request error", error);
|
|
1440
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1441
|
+
res.end(JSON.stringify({ error: message }));
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Read request body
|
|
1446
|
+
*/
|
|
1447
|
+
readBody(req) {
|
|
1448
|
+
return new Promise((resolve, reject) => {
|
|
1449
|
+
let body = "";
|
|
1450
|
+
req.on("data", (chunk) => body += chunk);
|
|
1451
|
+
req.on("end", () => resolve(body));
|
|
1452
|
+
req.on("error", reject);
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Set CORS headers
|
|
1457
|
+
*/
|
|
1458
|
+
setCorsHeaders(req, res) {
|
|
1459
|
+
const origin = req.headers.origin || "*";
|
|
1460
|
+
const allowedOrigin = this.config.corsOrigins.includes("*") ? "*" : this.config.corsOrigins.includes(origin) ? origin : this.config.corsOrigins[0];
|
|
1461
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
1462
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1463
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1464
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Debug logging
|
|
1468
|
+
*/
|
|
1469
|
+
log(message, data) {
|
|
1470
|
+
if (this.config.debug) {
|
|
1471
|
+
console.error(`[HTTPTransport] ${message}`, data ? JSON.stringify(data) : "");
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Get current session count
|
|
1476
|
+
*/
|
|
1477
|
+
getSessionCount() {
|
|
1478
|
+
return this.activeSessions.size;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Check if running
|
|
1482
|
+
*/
|
|
1483
|
+
isActive() {
|
|
1484
|
+
return this.isRunning;
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
async function createHTTPServer(mcpServer, config) {
|
|
1488
|
+
const transport = new HTTPTransport(config);
|
|
1489
|
+
await transport.start(mcpServer);
|
|
1490
|
+
return transport;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export { HTTPTransport, TopGunMCPServer, allTools, createHTTPServer, explainTool, listMapsTool, mutateTool, queryTool, schemaTool, searchTool, statsTool, subscribeTool, toolHandlers };
|
|
1494
|
+
//# sourceMappingURL=index.mjs.map
|
|
1495
|
+
//# sourceMappingURL=index.mjs.map
|