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