@vaultgradient/pipequery-cli 0.3.3 → 0.4.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 CHANGED
@@ -83,7 +83,7 @@ async function initCommand() {
83
83
 
84
84
  // src/commands/serve.ts
85
85
  import { writeFile as writeFile3, mkdir, readFile as readFile3, unlink } from "fs/promises";
86
- import { resolve as resolve4 } from "path";
86
+ import { resolve as resolve5 } from "path";
87
87
  import { existsSync as existsSync3 } from "fs";
88
88
  import { spawn } from "child_process";
89
89
 
@@ -236,11 +236,11 @@ var WebSocketSourceAdapter = class {
236
236
  listeners = /* @__PURE__ */ new Set();
237
237
  maxBuffer;
238
238
  async start() {
239
- return new Promise((resolve9, reject) => {
239
+ return new Promise((resolve10, reject) => {
240
240
  this.ws = new WebSocket(this.config.url);
241
241
  this.ws.on("open", () => {
242
242
  this.error = void 0;
243
- resolve9();
243
+ resolve10();
244
244
  });
245
245
  this.ws.on("message", (raw) => {
246
246
  try {
@@ -394,6 +394,455 @@ var StaticSourceAdapter = class {
394
394
  }
395
395
  };
396
396
 
397
+ // src/server/sources/postgres.ts
398
+ import pg from "pg";
399
+ var DEFAULT_INTERVAL_MS = 3e4;
400
+ var DEFAULT_MAX_ROWS = 1e4;
401
+ function expandEnv(input) {
402
+ return input.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => {
403
+ const value = process.env[name];
404
+ if (value === void 0) {
405
+ process.stderr.write(
406
+ `[pipequery] env var "${name}" referenced in postgres source URL is not set
407
+ `
408
+ );
409
+ return "";
410
+ }
411
+ return value;
412
+ });
413
+ }
414
+ function resolveSsl(ssl) {
415
+ if (ssl === false) return false;
416
+ if (ssl === "no-verify") return { rejectUnauthorized: false };
417
+ return { rejectUnauthorized: true };
418
+ }
419
+ var PostgresSourceAdapter = class {
420
+ constructor(config) {
421
+ this.config = config;
422
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS;
423
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS;
424
+ }
425
+ config;
426
+ pool = null;
427
+ data = [];
428
+ timer = null;
429
+ lastFetch = null;
430
+ error;
431
+ listeners = /* @__PURE__ */ new Set();
432
+ intervalMs;
433
+ maxRows;
434
+ async start() {
435
+ const connectionString = expandEnv(this.config.url);
436
+ this.pool = new pg.Pool({
437
+ connectionString,
438
+ ssl: resolveSsl(this.config.ssl),
439
+ // Small pool — we're read-only polling, not a transactional app.
440
+ min: 0,
441
+ max: 2,
442
+ idleTimeoutMillis: 1e4
443
+ });
444
+ this.pool.on("error", (err) => {
445
+ this.error = err.message;
446
+ });
447
+ await this.fetch();
448
+ this.timer = setInterval(() => {
449
+ void this.fetch();
450
+ }, this.intervalMs);
451
+ }
452
+ stop() {
453
+ if (this.timer) {
454
+ clearInterval(this.timer);
455
+ this.timer = null;
456
+ }
457
+ if (this.pool) {
458
+ void this.pool.end();
459
+ this.pool = null;
460
+ }
461
+ }
462
+ getData() {
463
+ return this.data;
464
+ }
465
+ getStatus() {
466
+ return {
467
+ healthy: !this.error,
468
+ rowCount: this.data.length,
469
+ lastFetch: this.lastFetch,
470
+ error: this.error
471
+ };
472
+ }
473
+ onUpdate(callback) {
474
+ this.listeners.add(callback);
475
+ return () => this.listeners.delete(callback);
476
+ }
477
+ async refresh() {
478
+ await this.fetch();
479
+ }
480
+ async fetch() {
481
+ if (!this.pool) {
482
+ this.error = "Postgres pool not initialized";
483
+ return;
484
+ }
485
+ try {
486
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
487
+ const res = await this.pool.query(capQuery);
488
+ if (res.rows.length > this.maxRows) {
489
+ throw new Error(
490
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
491
+ );
492
+ }
493
+ this.data = res.rows;
494
+ this.lastFetch = /* @__PURE__ */ new Date();
495
+ this.error = void 0;
496
+ this.notify();
497
+ } catch (err) {
498
+ this.error = err instanceof Error ? err.message : String(err);
499
+ }
500
+ }
501
+ notify() {
502
+ for (const cb of this.listeners) cb(this.data);
503
+ }
504
+ };
505
+ function stripTrailingSemicolon(sql) {
506
+ return sql.trim().replace(/;+\s*$/, "");
507
+ }
508
+
509
+ // src/server/sources/mysql.ts
510
+ import mysql from "mysql2/promise";
511
+ var DEFAULT_INTERVAL_MS2 = 3e4;
512
+ var DEFAULT_MAX_ROWS2 = 1e4;
513
+ function expandEnv2(input) {
514
+ return input.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => {
515
+ const value = process.env[name];
516
+ if (value === void 0) {
517
+ process.stderr.write(
518
+ `[pipequery] env var "${name}" referenced in mysql source URL is not set
519
+ `
520
+ );
521
+ return "";
522
+ }
523
+ return value;
524
+ });
525
+ }
526
+ function resolveSsl2(ssl) {
527
+ if (ssl === false) return void 0;
528
+ if (ssl === "no-verify") return { rejectUnauthorized: false };
529
+ return { rejectUnauthorized: true };
530
+ }
531
+ var MysqlSourceAdapter = class {
532
+ constructor(config) {
533
+ this.config = config;
534
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS2;
535
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS2;
536
+ }
537
+ config;
538
+ pool = null;
539
+ data = [];
540
+ timer = null;
541
+ lastFetch = null;
542
+ error;
543
+ listeners = /* @__PURE__ */ new Set();
544
+ intervalMs;
545
+ maxRows;
546
+ async start() {
547
+ const uri = expandEnv2(this.config.url);
548
+ this.pool = mysql.createPool({
549
+ uri,
550
+ ssl: resolveSsl2(this.config.ssl),
551
+ connectionLimit: 2,
552
+ waitForConnections: true,
553
+ // mysql2 returns BigInt for large integers by default — stringify so the
554
+ // data serializes cleanly to JSON (which can't encode BigInt).
555
+ supportBigNumbers: true,
556
+ bigNumberStrings: true
557
+ });
558
+ await this.fetch();
559
+ this.timer = setInterval(() => {
560
+ void this.fetch();
561
+ }, this.intervalMs);
562
+ }
563
+ stop() {
564
+ if (this.timer) {
565
+ clearInterval(this.timer);
566
+ this.timer = null;
567
+ }
568
+ if (this.pool) {
569
+ void this.pool.end();
570
+ this.pool = null;
571
+ }
572
+ }
573
+ getData() {
574
+ return this.data;
575
+ }
576
+ getStatus() {
577
+ return {
578
+ healthy: !this.error,
579
+ rowCount: this.data.length,
580
+ lastFetch: this.lastFetch,
581
+ error: this.error
582
+ };
583
+ }
584
+ onUpdate(callback) {
585
+ this.listeners.add(callback);
586
+ return () => this.listeners.delete(callback);
587
+ }
588
+ async refresh() {
589
+ await this.fetch();
590
+ }
591
+ async fetch() {
592
+ if (!this.pool) {
593
+ this.error = "MySQL pool not initialized";
594
+ return;
595
+ }
596
+ try {
597
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon2(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
598
+ const [rows] = await this.pool.query(capQuery);
599
+ const asArray = Array.isArray(rows) ? rows : [];
600
+ if (asArray.length > this.maxRows) {
601
+ throw new Error(
602
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
603
+ );
604
+ }
605
+ this.data = asArray;
606
+ this.lastFetch = /* @__PURE__ */ new Date();
607
+ this.error = void 0;
608
+ this.notify();
609
+ } catch (err) {
610
+ this.error = err instanceof Error ? err.message : String(err);
611
+ }
612
+ }
613
+ notify() {
614
+ for (const cb of this.listeners) cb(this.data);
615
+ }
616
+ };
617
+ function stripTrailingSemicolon2(sql) {
618
+ return sql.trim().replace(/;+\s*$/, "");
619
+ }
620
+
621
+ // src/server/sources/sqlite.ts
622
+ import { resolve as resolve4 } from "path";
623
+ import Database from "better-sqlite3";
624
+ var DEFAULT_INTERVAL_MS3 = 3e4;
625
+ var DEFAULT_MAX_ROWS3 = 1e4;
626
+ var IN_MEMORY = ":memory:";
627
+ var SqliteSourceAdapter = class {
628
+ constructor(config, cwd) {
629
+ this.config = config;
630
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS3;
631
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS3;
632
+ this.readonly = config.readonly ?? true;
633
+ this.resolvedPath = config.path === IN_MEMORY ? IN_MEMORY : resolve4(cwd, config.path);
634
+ }
635
+ config;
636
+ db = null;
637
+ data = [];
638
+ timer = null;
639
+ lastFetch = null;
640
+ error;
641
+ listeners = /* @__PURE__ */ new Set();
642
+ intervalMs;
643
+ maxRows;
644
+ resolvedPath;
645
+ readonly;
646
+ async start() {
647
+ const opts = this.resolvedPath === IN_MEMORY ? {} : { readonly: this.readonly, fileMustExist: this.readonly };
648
+ this.db = new Database(this.resolvedPath, opts);
649
+ if (this.resolvedPath !== IN_MEMORY && !this.readonly) {
650
+ this.db.pragma("journal_mode = WAL");
651
+ }
652
+ await this.fetch();
653
+ this.timer = setInterval(() => {
654
+ void this.fetch();
655
+ }, this.intervalMs);
656
+ }
657
+ stop() {
658
+ if (this.timer) {
659
+ clearInterval(this.timer);
660
+ this.timer = null;
661
+ }
662
+ if (this.db) {
663
+ this.db.close();
664
+ this.db = null;
665
+ }
666
+ }
667
+ getData() {
668
+ return this.data;
669
+ }
670
+ getStatus() {
671
+ return {
672
+ healthy: !this.error,
673
+ rowCount: this.data.length,
674
+ lastFetch: this.lastFetch,
675
+ error: this.error
676
+ };
677
+ }
678
+ onUpdate(callback) {
679
+ this.listeners.add(callback);
680
+ return () => this.listeners.delete(callback);
681
+ }
682
+ async refresh() {
683
+ await this.fetch();
684
+ }
685
+ async fetch() {
686
+ if (!this.db) {
687
+ this.error = "SQLite database not initialized";
688
+ return;
689
+ }
690
+ try {
691
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon3(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
692
+ const rows = this.db.prepare(capQuery).all();
693
+ if (rows.length > this.maxRows) {
694
+ throw new Error(
695
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
696
+ );
697
+ }
698
+ this.data = rows;
699
+ this.lastFetch = /* @__PURE__ */ new Date();
700
+ this.error = void 0;
701
+ this.notify();
702
+ } catch (err) {
703
+ this.error = err instanceof Error ? err.message : String(err);
704
+ }
705
+ }
706
+ notify() {
707
+ for (const cb of this.listeners) cb(this.data);
708
+ }
709
+ };
710
+ function stripTrailingSemicolon3(sql) {
711
+ return sql.trim().replace(/;+\s*$/, "");
712
+ }
713
+
714
+ // src/server/sources/kafka.ts
715
+ import { randomBytes } from "crypto";
716
+ import { Kafka, logLevel } from "kafkajs";
717
+ var DEFAULT_MAX_BUFFER = 1e3;
718
+ function expandEnv3(input) {
719
+ return input.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => {
720
+ const value = process.env[name];
721
+ if (value === void 0) {
722
+ process.stderr.write(
723
+ `[pipequery] env var "${name}" referenced in kafka source is not set
724
+ `
725
+ );
726
+ return "";
727
+ }
728
+ return value;
729
+ });
730
+ }
731
+ function normalizeBrokers(brokers) {
732
+ const list = Array.isArray(brokers) ? brokers : brokers.split(",");
733
+ return list.map((b) => expandEnv3(b.trim())).filter(Boolean);
734
+ }
735
+ var KafkaSourceAdapter = class {
736
+ constructor(config) {
737
+ this.config = config;
738
+ this.maxBuffer = config.maxBuffer ?? DEFAULT_MAX_BUFFER;
739
+ this.groupId = config.groupId ?? `pipequery-${randomBytes(6).toString("hex")}`;
740
+ }
741
+ config;
742
+ data = [];
743
+ kafka = null;
744
+ consumer = null;
745
+ lastFetch = null;
746
+ error;
747
+ connected = false;
748
+ listeners = /* @__PURE__ */ new Set();
749
+ maxBuffer;
750
+ groupId;
751
+ async start() {
752
+ const brokers = normalizeBrokers(this.config.brokers);
753
+ if (brokers.length === 0) {
754
+ throw new Error("Kafka source has no brokers configured");
755
+ }
756
+ this.kafka = new Kafka({
757
+ clientId: `pipequery-${this.config.topic}`,
758
+ brokers,
759
+ ssl: this.config.ssl ?? false,
760
+ sasl: this.config.sasl ? {
761
+ mechanism: this.config.sasl.mechanism,
762
+ username: expandEnv3(this.config.sasl.username),
763
+ password: expandEnv3(this.config.sasl.password)
764
+ } : void 0,
765
+ // Silence kafkajs's own logger — we surface errors via SourceStatus.
766
+ logLevel: logLevel.ERROR
767
+ });
768
+ this.consumer = this.kafka.consumer({ groupId: this.groupId });
769
+ await this.consumer.connect();
770
+ await this.consumer.subscribe({
771
+ topic: this.config.topic,
772
+ fromBeginning: this.config.fromBeginning ?? false
773
+ });
774
+ this.connected = true;
775
+ this.error = void 0;
776
+ void this.consumer.run({
777
+ eachMessage: async ({ message, partition }) => {
778
+ try {
779
+ const row = this.decode(message.value, message.key, message.timestamp, partition, message.offset);
780
+ this.data.push(row);
781
+ if (this.data.length > this.maxBuffer) {
782
+ this.data = this.data.slice(-this.maxBuffer);
783
+ }
784
+ this.lastFetch = /* @__PURE__ */ new Date();
785
+ this.error = void 0;
786
+ this.notify();
787
+ } catch {
788
+ }
789
+ }
790
+ });
791
+ }
792
+ stop() {
793
+ this.connected = false;
794
+ if (this.consumer) {
795
+ const consumer = this.consumer;
796
+ this.consumer = null;
797
+ void consumer.disconnect().catch(() => {
798
+ });
799
+ }
800
+ this.kafka = null;
801
+ }
802
+ getData() {
803
+ return this.data;
804
+ }
805
+ getStatus() {
806
+ return {
807
+ healthy: this.connected && !this.error,
808
+ rowCount: this.data.length,
809
+ lastFetch: this.lastFetch,
810
+ error: this.error
811
+ };
812
+ }
813
+ onUpdate(callback) {
814
+ this.listeners.add(callback);
815
+ return () => this.listeners.delete(callback);
816
+ }
817
+ decode(value, key, timestamp, partition, offset) {
818
+ const base = {
819
+ _kafka_topic: this.config.topic,
820
+ _kafka_partition: partition,
821
+ _kafka_offset: offset,
822
+ _kafka_timestamp: Number.parseInt(timestamp, 10),
823
+ _kafka_key: key ? key.toString("utf8") : null
824
+ };
825
+ if (!value) {
826
+ return { ...base, value: null };
827
+ }
828
+ const format = this.config.valueFormat ?? "json";
829
+ if (format === "string") {
830
+ return { ...base, value: value.toString("utf8") };
831
+ }
832
+ if (format === "raw") {
833
+ return { ...base, value: value.toString("base64") };
834
+ }
835
+ const decoded = JSON.parse(value.toString("utf8"));
836
+ if (decoded !== null && typeof decoded === "object" && !Array.isArray(decoded)) {
837
+ return { ...base, ...decoded };
838
+ }
839
+ return { ...base, value: decoded };
840
+ }
841
+ notify() {
842
+ for (const cb of this.listeners) cb(this.data);
843
+ }
844
+ };
845
+
397
846
  // src/server/sources/manager.ts
398
847
  var SourceManager = class {
399
848
  constructor(cwd) {
@@ -479,6 +928,14 @@ var SourceManager = class {
479
928
  return new FileSourceAdapter(config, this.cwd);
480
929
  case "static":
481
930
  return new StaticSourceAdapter(config);
931
+ case "postgres":
932
+ return new PostgresSourceAdapter(config);
933
+ case "mysql":
934
+ return new MysqlSourceAdapter(config);
935
+ case "sqlite":
936
+ return new SqliteSourceAdapter(config, this.cwd);
937
+ case "kafka":
938
+ return new KafkaSourceAdapter(config);
482
939
  default:
483
940
  throw new Error(`Unknown source type: ${config.type}`);
484
941
  }
@@ -2665,8 +3122,8 @@ function printServerStartup(opts) {
2665
3122
  // src/commands/serve.ts
2666
3123
  async function serveCommand(opts) {
2667
3124
  const cwd = process.cwd();
2668
- const pqDir = resolve4(cwd, ".pipequery");
2669
- const serverJsonPath = resolve4(pqDir, "server.json");
3125
+ const pqDir = resolve5(cwd, ".pipequery");
3126
+ const serverJsonPath = resolve5(pqDir, "server.json");
2670
3127
  if (opts.stop) {
2671
3128
  return stopDaemon(serverJsonPath);
2672
3129
  }
@@ -2678,7 +3135,7 @@ async function serveCommand(opts) {
2678
3135
  }
2679
3136
  const { app, sourceManager } = await createServer(config, configPath, cwd);
2680
3137
  await mkdir(pqDir, { recursive: true });
2681
- const logPath = resolve4(pqDir, "server.log");
3138
+ const logPath = resolve5(pqDir, "server.log");
2682
3139
  await writeFile3(serverJsonPath, JSON.stringify({ url: `http://localhost:${port}`, pid: process.pid, daemon: false, log: logPath }));
2683
3140
  await app.listen({ port, host });
2684
3141
  const sourceCount = sourceManager.getSourceNames().length;
@@ -2700,9 +3157,9 @@ async function serveCommand(opts) {
2700
3157
  process.on("SIGTERM", shutdown);
2701
3158
  }
2702
3159
  async function startDaemon(cwd, port, host) {
2703
- const pqDir = resolve4(cwd, ".pipequery");
2704
- const serverJsonPath = resolve4(pqDir, "server.json");
2705
- const logPath = resolve4(pqDir, "server.log");
3160
+ const pqDir = resolve5(cwd, ".pipequery");
3161
+ const serverJsonPath = resolve5(pqDir, "server.json");
3162
+ const logPath = resolve5(pqDir, "server.log");
2706
3163
  if (existsSync3(serverJsonPath)) {
2707
3164
  try {
2708
3165
  const info2 = JSON.parse(await readFile3(serverJsonPath, "utf-8"));
@@ -2759,11 +3216,11 @@ import chalk4 from "chalk";
2759
3216
 
2760
3217
  // src/utils/server-discovery.ts
2761
3218
  import { readFile as readFile4 } from "fs/promises";
2762
- import { resolve as resolve5 } from "path";
3219
+ import { resolve as resolve6 } from "path";
2763
3220
  import { existsSync as existsSync4 } from "fs";
2764
3221
  async function getServerUrl() {
2765
3222
  const cwd = process.cwd();
2766
- const serverJsonPath = resolve5(cwd, ".pipequery", "server.json");
3223
+ const serverJsonPath = resolve6(cwd, ".pipequery", "server.json");
2767
3224
  if (existsSync4(serverJsonPath)) {
2768
3225
  try {
2769
3226
  const info2 = JSON.parse(await readFile4(serverJsonPath, "utf-8"));
@@ -2906,8 +3363,60 @@ async function sourceAddCommand(name, opts) {
2906
3363
  ...opts.watch ? { watch: true } : {}
2907
3364
  };
2908
3365
  break;
3366
+ case "postgres":
3367
+ if (!opts.url) throw new Error("--url is required for Postgres sources");
3368
+ if (!opts.query) throw new Error("--query is required for Postgres sources");
3369
+ sourceConfig = {
3370
+ type: "postgres",
3371
+ url: opts.url,
3372
+ query: opts.query,
3373
+ interval: opts.interval ?? "30s",
3374
+ ...opts.ssl === "no-verify" ? { ssl: "no-verify" } : {},
3375
+ ...opts.ssl === "false" ? { ssl: false } : {},
3376
+ ...opts.maxRows ? { maxRows: Number.parseInt(opts.maxRows, 10) } : {}
3377
+ };
3378
+ break;
3379
+ case "mysql":
3380
+ if (!opts.url) throw new Error("--url is required for MySQL sources");
3381
+ if (!opts.query) throw new Error("--query is required for MySQL sources");
3382
+ sourceConfig = {
3383
+ type: "mysql",
3384
+ url: opts.url,
3385
+ query: opts.query,
3386
+ interval: opts.interval ?? "30s",
3387
+ ...opts.ssl === "no-verify" ? { ssl: "no-verify" } : {},
3388
+ ...opts.ssl === "false" ? { ssl: false } : {},
3389
+ ...opts.maxRows ? { maxRows: Number.parseInt(opts.maxRows, 10) } : {}
3390
+ };
3391
+ break;
3392
+ case "sqlite":
3393
+ if (!opts.path) throw new Error("--path is required for SQLite sources");
3394
+ if (!opts.query) throw new Error("--query is required for SQLite sources");
3395
+ sourceConfig = {
3396
+ type: "sqlite",
3397
+ path: opts.path,
3398
+ query: opts.query,
3399
+ interval: opts.interval ?? "30s",
3400
+ ...opts.maxRows ? { maxRows: Number.parseInt(opts.maxRows, 10) } : {},
3401
+ ...opts.readonly === false ? { readonly: false } : {}
3402
+ };
3403
+ break;
3404
+ case "kafka":
3405
+ if (!opts.brokers) throw new Error("--brokers is required for Kafka sources (comma-separated)");
3406
+ if (!opts.topic) throw new Error("--topic is required for Kafka sources");
3407
+ sourceConfig = {
3408
+ type: "kafka",
3409
+ brokers: opts.brokers,
3410
+ topic: opts.topic,
3411
+ ...opts.groupId ? { groupId: opts.groupId } : {},
3412
+ ...opts.fromBeginning ? { fromBeginning: true } : {},
3413
+ ...opts.maxBuffer ? { maxBuffer: Number.parseInt(opts.maxBuffer, 10) } : {},
3414
+ ...opts.valueFormat ? { valueFormat: opts.valueFormat } : {},
3415
+ ...opts.ssl === "true" ? { ssl: true } : {}
3416
+ };
3417
+ break;
2909
3418
  default:
2910
- throw new Error(`Unknown source type: ${opts.type}. Use: rest, websocket, file`);
3419
+ throw new Error(`Unknown source type: ${opts.type}. Use: rest, websocket, file, postgres, mysql, sqlite, kafka`);
2911
3420
  }
2912
3421
  config.sources[name] = sourceConfig;
2913
3422
  await saveConfig(config, configPath);
@@ -3862,7 +4371,7 @@ async function dashboardCommand(opts) {
3862
4371
  import React6 from "react";
3863
4372
  import { render as render2 } from "ink";
3864
4373
  import { existsSync as existsSync5, readFileSync } from "fs";
3865
- import { resolve as resolve6 } from "path";
4374
+ import { resolve as resolve7 } from "path";
3866
4375
 
3867
4376
  // src/tui/MonitorApp.tsx
3868
4377
  import { useState as useState5, useEffect as useEffect4 } from "react";
@@ -4046,7 +4555,7 @@ function MonitorApp({ serverUrl }) {
4046
4555
  // src/commands/monitor.ts
4047
4556
  async function monitorCommand() {
4048
4557
  const serverUrl = await getServerUrl();
4049
- const discoveryPath = resolve6(process.cwd(), ".pipequery", "server.json");
4558
+ const discoveryPath = resolve7(process.cwd(), ".pipequery", "server.json");
4050
4559
  let logPath;
4051
4560
  if (existsSync5(discoveryPath)) {
4052
4561
  try {
@@ -4058,7 +4567,7 @@ async function monitorCommand() {
4058
4567
  }
4059
4568
  }
4060
4569
  if (!logPath) {
4061
- const fallback = resolve6(process.cwd(), ".pipequery", "server.log");
4570
+ const fallback = resolve7(process.cwd(), ".pipequery", "server.log");
4062
4571
  if (existsSync5(fallback)) {
4063
4572
  logPath = fallback;
4064
4573
  }
@@ -4078,7 +4587,7 @@ async function monitorCommand() {
4078
4587
 
4079
4588
  // src/commands/remote.ts
4080
4589
  import { writeFile as writeFile4 } from "fs/promises";
4081
- import { resolve as resolve7 } from "path";
4590
+ import { resolve as resolve8 } from "path";
4082
4591
 
4083
4592
  // src/docker/templates.ts
4084
4593
  function generateDockerfile(port) {
@@ -4128,9 +4637,9 @@ async function remoteDeployCommand() {
4128
4637
  const cwd = process.cwd();
4129
4638
  const { config } = await loadConfig(cwd);
4130
4639
  const port = config.server.port;
4131
- await writeFile4(resolve7(cwd, "Dockerfile"), generateDockerfile(port));
4132
- await writeFile4(resolve7(cwd, "docker-compose.yaml"), generateDockerCompose(port));
4133
- await writeFile4(resolve7(cwd, ".dockerignore"), generateDockerignore());
4640
+ await writeFile4(resolve8(cwd, "Dockerfile"), generateDockerfile(port));
4641
+ await writeFile4(resolve8(cwd, "docker-compose.yaml"), generateDockerCompose(port));
4642
+ await writeFile4(resolve8(cwd, ".dockerignore"), generateDockerignore());
4134
4643
  log.success("Generated Dockerfile, docker-compose.yaml, and .dockerignore");
4135
4644
  log.info(`Run: docker compose up -d --build`);
4136
4645
  log.info(`Then: pq remote connect http://localhost:${port}`);
@@ -4140,7 +4649,7 @@ async function loadOrCreateConfig(cwd) {
4140
4649
  if (existing) {
4141
4650
  return loadConfig(cwd);
4142
4651
  }
4143
- const configPath = resolve7(cwd, "pipequery.yaml");
4652
+ const configPath = resolve8(cwd, "pipequery.yaml");
4144
4653
  const config = {
4145
4654
  server: { ...DEFAULT_CONFIG.server },
4146
4655
  sources: {},
@@ -4188,6 +4697,479 @@ async function remoteStatusCommand() {
4188
4697
  }
4189
4698
  }
4190
4699
 
4700
+ // src/mcp/provider.ts
4701
+ var LocalProvider = class {
4702
+ constructor(config, cwd) {
4703
+ this.config = config;
4704
+ this.sourceManager = new SourceManager(cwd);
4705
+ this.endpoints = new Map(Object.entries(config.endpoints));
4706
+ this.ready = this.bootSources();
4707
+ }
4708
+ config;
4709
+ sourceManager;
4710
+ endpoints;
4711
+ ready;
4712
+ async bootSources() {
4713
+ for (const [name, src] of Object.entries(this.config.sources)) {
4714
+ try {
4715
+ await this.sourceManager.addSource(name, src);
4716
+ } catch (err) {
4717
+ const msg = err instanceof Error ? err.message : String(err);
4718
+ process.stderr.write(`[pipequery-mcp] Source "${name}" failed to start: ${msg}
4719
+ `);
4720
+ }
4721
+ }
4722
+ }
4723
+ async listSources() {
4724
+ await this.ready;
4725
+ const statuses = this.sourceManager.getAllStatuses();
4726
+ return Object.entries(statuses).map(([name, status]) => ({ name, status }));
4727
+ }
4728
+ async describeSource(name, sampleSize = 5) {
4729
+ await this.ready;
4730
+ const status = this.sourceManager.getSourceStatus(name);
4731
+ const data = this.sourceManager.getSourceData(name);
4732
+ if (!status || !data) return void 0;
4733
+ const sample = data.slice(0, sampleSize);
4734
+ const fields = inferFields(data);
4735
+ return { name, status, sample, fields };
4736
+ }
4737
+ async listEndpoints() {
4738
+ await this.ready;
4739
+ return [...this.endpoints].map(([path, config]) => ({ path, config }));
4740
+ }
4741
+ async callEndpoint(path) {
4742
+ await this.ready;
4743
+ const endpoint2 = this.endpoints.get(path);
4744
+ if (!endpoint2) {
4745
+ throw new Error(`Endpoint "${path}" not found`);
4746
+ }
4747
+ const context = this.sourceManager.getContext();
4748
+ return query(context, endpoint2.query);
4749
+ }
4750
+ async runQuery(expression) {
4751
+ await this.ready;
4752
+ const context = this.sourceManager.getContext();
4753
+ return query(context, expression);
4754
+ }
4755
+ async dispose() {
4756
+ await this.sourceManager.dispose();
4757
+ }
4758
+ // Exposed for the HTTP plugin that wants to mount the MCP server into an
4759
+ // existing `createServer` instance; the caller passes in their own manager.
4760
+ static withSourceManager(sourceManager, endpoints) {
4761
+ return new AttachedLocalProvider(sourceManager, endpoints);
4762
+ }
4763
+ };
4764
+ var AttachedLocalProvider = class {
4765
+ constructor(sourceManager, endpoints) {
4766
+ this.sourceManager = sourceManager;
4767
+ this.endpoints = endpoints;
4768
+ }
4769
+ sourceManager;
4770
+ endpoints;
4771
+ async listSources() {
4772
+ const statuses = this.sourceManager.getAllStatuses();
4773
+ return Object.entries(statuses).map(([name, status]) => ({ name, status }));
4774
+ }
4775
+ async describeSource(name, sampleSize = 5) {
4776
+ const status = this.sourceManager.getSourceStatus(name);
4777
+ const data = this.sourceManager.getSourceData(name);
4778
+ if (!status || !data) return void 0;
4779
+ return { name, status, sample: data.slice(0, sampleSize), fields: inferFields(data) };
4780
+ }
4781
+ async listEndpoints() {
4782
+ return [...this.endpoints].map(([path, config]) => ({ path, config }));
4783
+ }
4784
+ async callEndpoint(path) {
4785
+ const endpoint2 = this.endpoints.get(path);
4786
+ if (!endpoint2) throw new Error(`Endpoint "${path}" not found`);
4787
+ return query(this.sourceManager.getContext(), endpoint2.query);
4788
+ }
4789
+ async runQuery(expression) {
4790
+ return query(this.sourceManager.getContext(), expression);
4791
+ }
4792
+ async dispose() {
4793
+ }
4794
+ };
4795
+ var AttachedProvider = class {
4796
+ constructor(baseUrl) {
4797
+ this.baseUrl = baseUrl;
4798
+ this.baseUrl = baseUrl.replace(/\/$/, "");
4799
+ }
4800
+ baseUrl;
4801
+ async get(path) {
4802
+ const res = await fetch(`${this.baseUrl}${path}`);
4803
+ if (!res.ok) throw new Error(`GET ${path} failed: HTTP ${res.status}`);
4804
+ return res.json();
4805
+ }
4806
+ async post(path, body) {
4807
+ const res = await fetch(`${this.baseUrl}${path}`, {
4808
+ method: "POST",
4809
+ headers: { "content-type": "application/json" },
4810
+ body: JSON.stringify(body)
4811
+ });
4812
+ if (!res.ok) throw new Error(`POST ${path} failed: HTTP ${res.status}`);
4813
+ return res.json();
4814
+ }
4815
+ async listSources() {
4816
+ const statuses = await this.get("/api/_control/sources");
4817
+ return Object.entries(statuses).map(([name, status]) => ({ name, status }));
4818
+ }
4819
+ async describeSource(name, sampleSize = 5) {
4820
+ const res = await this.post(`/api/_control/sources/${encodeURIComponent(name)}/test`, {});
4821
+ if (!res.ok || !res.status || !res.sample) return void 0;
4822
+ return {
4823
+ name,
4824
+ status: res.status,
4825
+ sample: res.sample.slice(0, sampleSize),
4826
+ fields: inferFields(res.sample)
4827
+ };
4828
+ }
4829
+ async listEndpoints() {
4830
+ const endpoints = await this.get("/api/_control/endpoints");
4831
+ return Object.entries(endpoints).map(([path, config]) => ({ path, config }));
4832
+ }
4833
+ async callEndpoint(path) {
4834
+ const normalized = path.startsWith("/") ? path : `/${path}`;
4835
+ const res = await fetch(`${this.baseUrl}${normalized}`);
4836
+ if (!res.ok) throw new Error(`GET ${normalized} failed: HTTP ${res.status}`);
4837
+ return res.json();
4838
+ }
4839
+ async runQuery(expression) {
4840
+ const res = await this.post(
4841
+ "/api/_control/query",
4842
+ { query: expression }
4843
+ );
4844
+ if (!res.ok) throw new Error(res.error ?? "Query failed");
4845
+ return res.result;
4846
+ }
4847
+ async dispose() {
4848
+ }
4849
+ };
4850
+ function inferFields(rows) {
4851
+ const fields = /* @__PURE__ */ new Set();
4852
+ for (const row of rows.slice(0, 20)) {
4853
+ if (row && typeof row === "object" && !Array.isArray(row)) {
4854
+ for (const k of Object.keys(row)) fields.add(k);
4855
+ }
4856
+ }
4857
+ return [...fields];
4858
+ }
4859
+
4860
+ // src/mcp/stdio.ts
4861
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4862
+
4863
+ // src/mcp/server.ts
4864
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4865
+ import { z } from "zod";
4866
+ var DEFAULT_INFO = {
4867
+ name: "pipequery",
4868
+ version: "0.1.0"
4869
+ };
4870
+ function createMcpServer(provider, info2 = DEFAULT_INFO) {
4871
+ const mcp2 = new McpServer(info2);
4872
+ mcp2.registerTool(
4873
+ "query",
4874
+ {
4875
+ title: "Run a PipeQuery expression",
4876
+ description: "Execute a PipeQuery pipe-based expression (e.g. `crypto | where(price > 100) | sort(price desc) | first(5)`) against all currently configured sources. Returns the computed result as JSON.",
4877
+ inputSchema: {
4878
+ expression: z.string().min(1).describe("The PipeQuery expression. Source names used in the expression must be configured; call `list_sources` to see them.")
4879
+ }
4880
+ },
4881
+ async ({ expression }) => {
4882
+ const result = await provider.runQuery(expression);
4883
+ return toolResult(result);
4884
+ }
4885
+ );
4886
+ mcp2.registerTool(
4887
+ "list_sources",
4888
+ {
4889
+ title: "List configured data sources",
4890
+ description: "List every data source configured in pipequery.yaml along with its current health, row count, and last-fetch timestamp."
4891
+ },
4892
+ async () => {
4893
+ const sources = await provider.listSources();
4894
+ return toolResult({ count: sources.length, sources });
4895
+ }
4896
+ );
4897
+ mcp2.registerTool(
4898
+ "describe_source",
4899
+ {
4900
+ title: "Describe a data source",
4901
+ description: "Return a sample of rows and the inferred field names for a single source. Useful before constructing a query.",
4902
+ inputSchema: {
4903
+ name: z.string().min(1).describe("Source name as returned by list_sources."),
4904
+ sample_size: z.number().int().positive().max(100).optional().describe("Number of rows to include in the sample. Defaults to 5, max 100.")
4905
+ }
4906
+ },
4907
+ async ({ name, sample_size }) => {
4908
+ const description = await provider.describeSource(name, sample_size);
4909
+ if (!description) {
4910
+ return toolError(`Source "${name}" not found`);
4911
+ }
4912
+ return toolResult(description);
4913
+ }
4914
+ );
4915
+ mcp2.registerTool(
4916
+ "list_endpoints",
4917
+ {
4918
+ title: "List pre-configured query endpoints",
4919
+ description: "Return every endpoint declared in pipequery.yaml along with its stored query expression. These endpoints can be invoked via `call_endpoint`."
4920
+ },
4921
+ async () => {
4922
+ const endpoints = await provider.listEndpoints();
4923
+ return toolResult({ count: endpoints.length, endpoints });
4924
+ }
4925
+ );
4926
+ mcp2.registerTool(
4927
+ "call_endpoint",
4928
+ {
4929
+ title: "Invoke a pre-configured endpoint",
4930
+ description: "Execute the stored query for an endpoint path (e.g. `/api/top-coins`) and return its JSON result. Equivalent to the HTTP endpoint on `pq serve`, without needing a live HTTP call.",
4931
+ inputSchema: {
4932
+ path: z.string().min(1).describe("Endpoint path, exactly as declared in pipequery.yaml (typically begins with `/`).")
4933
+ }
4934
+ },
4935
+ async ({ path }) => {
4936
+ try {
4937
+ const result = await provider.callEndpoint(path);
4938
+ return toolResult(result);
4939
+ } catch (err) {
4940
+ return toolError(err instanceof Error ? err.message : String(err));
4941
+ }
4942
+ }
4943
+ );
4944
+ return mcp2;
4945
+ }
4946
+ function toolResult(data) {
4947
+ return {
4948
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
4949
+ };
4950
+ }
4951
+ function toolError(message) {
4952
+ return {
4953
+ content: [{ type: "text", text: message }],
4954
+ isError: true
4955
+ };
4956
+ }
4957
+
4958
+ // src/mcp/stdio.ts
4959
+ async function runStdio(provider, info2) {
4960
+ const mcp2 = createMcpServer(provider, info2);
4961
+ const transport = new StdioServerTransport();
4962
+ await mcp2.connect(transport);
4963
+ const cleanup = async () => {
4964
+ try {
4965
+ await mcp2.close();
4966
+ } finally {
4967
+ await provider.dispose();
4968
+ }
4969
+ };
4970
+ process.stdin.on("close", () => {
4971
+ void cleanup().then(() => process.exit(0));
4972
+ });
4973
+ process.on("SIGINT", () => void cleanup().then(() => process.exit(0)));
4974
+ process.on("SIGTERM", () => void cleanup().then(() => process.exit(0)));
4975
+ }
4976
+
4977
+ // src/mcp/http.ts
4978
+ import { createServer as createHttpServer } from "http";
4979
+ import { randomUUID } from "crypto";
4980
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4981
+ async function runHttp(provider, opts) {
4982
+ const host = opts.host ?? "127.0.0.1";
4983
+ const port = opts.port;
4984
+ const mountPath = opts.path ?? "/mcp";
4985
+ const transports = /* @__PURE__ */ new Map();
4986
+ const authToken = opts.authToken;
4987
+ if (!authToken) {
4988
+ process.stderr.write(
4989
+ "[pipequery-mcp] WARNING: HTTP endpoint is UNAUTHENTICATED. Set --auth-token or PIPEQUERY_MCP_TOKEN for anything beyond localhost.\n"
4990
+ );
4991
+ }
4992
+ const attachTransport = async () => {
4993
+ const transport = new StreamableHTTPServerTransport({
4994
+ sessionIdGenerator: () => randomUUID(),
4995
+ onsessioninitialized: (sessionId) => {
4996
+ transports.set(sessionId, transport);
4997
+ },
4998
+ onsessionclosed: (sessionId) => {
4999
+ transports.delete(sessionId);
5000
+ }
5001
+ });
5002
+ const mcp2 = createMcpServer(provider, opts.info);
5003
+ await mcp2.connect(transport);
5004
+ return transport;
5005
+ };
5006
+ const server = createHttpServer(async (req, res) => {
5007
+ if (!req.url) {
5008
+ res.statusCode = 400;
5009
+ res.end("Bad Request");
5010
+ return;
5011
+ }
5012
+ const requestPath = req.url.split("?")[0];
5013
+ if (requestPath !== mountPath) {
5014
+ res.statusCode = 404;
5015
+ res.end("Not Found");
5016
+ return;
5017
+ }
5018
+ if (authToken && !isAuthorized(req, authToken)) {
5019
+ res.statusCode = 401;
5020
+ res.setHeader("content-type", "application/json");
5021
+ res.setHeader("www-authenticate", 'Bearer realm="pipequery-mcp"');
5022
+ res.end(
5023
+ JSON.stringify({
5024
+ jsonrpc: "2.0",
5025
+ error: { code: -32001, message: "Unauthorized: missing or invalid bearer token" },
5026
+ id: null
5027
+ })
5028
+ );
5029
+ return;
5030
+ }
5031
+ try {
5032
+ const body = await readJsonBody(req);
5033
+ const sessionId = getSessionId(req);
5034
+ let transport = sessionId ? transports.get(sessionId) : void 0;
5035
+ if (!transport) {
5036
+ transport = await attachTransport();
5037
+ }
5038
+ await transport.handleRequest(req, res, body);
5039
+ } catch (err) {
5040
+ process.stderr.write(
5041
+ `[pipequery-mcp] transport error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
5042
+ `
5043
+ );
5044
+ if (!res.headersSent) {
5045
+ res.statusCode = 500;
5046
+ res.setHeader("content-type", "application/json");
5047
+ res.end(JSON.stringify({
5048
+ jsonrpc: "2.0",
5049
+ error: {
5050
+ code: -32603,
5051
+ message: err instanceof Error ? err.message : String(err)
5052
+ },
5053
+ id: null
5054
+ }));
5055
+ } else {
5056
+ res.end();
5057
+ }
5058
+ }
5059
+ });
5060
+ await new Promise((resolve10) => server.listen(port, host, resolve10));
5061
+ return {
5062
+ url: `http://${host}:${port}${mountPath}`,
5063
+ async close() {
5064
+ await new Promise((resolve10, reject) => {
5065
+ server.close((err) => err ? reject(err) : resolve10());
5066
+ });
5067
+ for (const t of transports.values()) {
5068
+ await t.close();
5069
+ }
5070
+ transports.clear();
5071
+ await provider.dispose();
5072
+ }
5073
+ };
5074
+ }
5075
+ function isAuthorized(req, expected) {
5076
+ const header = req.headers["authorization"];
5077
+ const raw = typeof header === "string" ? header : Array.isArray(header) ? header[0] : void 0;
5078
+ if (!raw) return false;
5079
+ const match = raw.match(/^Bearer\s+(.+)$/i);
5080
+ if (!match) return false;
5081
+ const token = match[1].trim();
5082
+ return timingSafeEqual(token, expected);
5083
+ }
5084
+ function timingSafeEqual(a, b) {
5085
+ if (a.length !== b.length) return false;
5086
+ let diff = 0;
5087
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
5088
+ return diff === 0;
5089
+ }
5090
+ function getSessionId(req) {
5091
+ const header = req.headers["mcp-session-id"];
5092
+ if (typeof header === "string") return header;
5093
+ if (Array.isArray(header)) return header[0];
5094
+ return void 0;
5095
+ }
5096
+ async function readJsonBody(req) {
5097
+ const method = req.method?.toUpperCase();
5098
+ if (method === "GET" || method === "DELETE") return void 0;
5099
+ const chunks = [];
5100
+ for await (const chunk of req) {
5101
+ chunks.push(chunk);
5102
+ }
5103
+ if (chunks.length === 0) return void 0;
5104
+ const text = Buffer.concat(chunks).toString("utf8");
5105
+ if (!text.trim()) return void 0;
5106
+ try {
5107
+ return JSON.parse(text);
5108
+ } catch {
5109
+ throw new Error("Invalid JSON body");
5110
+ }
5111
+ }
5112
+
5113
+ // src/commands/mcp.ts
5114
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5115
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5116
+ async function mcpServeCommand(opts) {
5117
+ const provider = await buildProvider(opts.attach);
5118
+ const isStdio = !opts.http;
5119
+ if (isStdio) {
5120
+ await runStdio(provider);
5121
+ return;
5122
+ }
5123
+ const port = opts.port ?? 3001;
5124
+ const host = opts.host ?? "127.0.0.1";
5125
+ const authToken = opts.authToken ?? process.env.PIPEQUERY_MCP_TOKEN;
5126
+ const handle = await runHttp(provider, { port, host, authToken });
5127
+ log.success(`PipeQuery MCP server listening on ${handle.url}`);
5128
+ log.info("Connect any MCP client to the URL above (Claude, Cursor, custom agents).");
5129
+ log.info("Press Ctrl+C to stop.");
5130
+ const shutdown = async () => {
5131
+ log.info("Shutting down MCP server...");
5132
+ await handle.close();
5133
+ process.exit(0);
5134
+ };
5135
+ process.on("SIGINT", () => void shutdown());
5136
+ process.on("SIGTERM", () => void shutdown());
5137
+ }
5138
+ async function mcpInspectCommand(opts) {
5139
+ const provider = await buildProvider(opts.attach);
5140
+ const mcp2 = createMcpServer(provider);
5141
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
5142
+ await mcp2.connect(serverTransport);
5143
+ const client = new Client({ name: "pq-mcp-inspect", version: "0.1.0" });
5144
+ await client.connect(clientTransport);
5145
+ try {
5146
+ const { tools } = await client.listTools();
5147
+ const summary = {
5148
+ server: "pipequery",
5149
+ toolCount: tools.length,
5150
+ tools: tools.map((t) => ({
5151
+ name: t.name,
5152
+ title: t.title,
5153
+ description: t.description,
5154
+ inputSchema: t.inputSchema
5155
+ }))
5156
+ };
5157
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
5158
+ } finally {
5159
+ await client.close();
5160
+ await mcp2.close();
5161
+ await provider.dispose();
5162
+ }
5163
+ }
5164
+ async function buildProvider(attachUrl) {
5165
+ if (attachUrl) {
5166
+ return new AttachedProvider(attachUrl);
5167
+ }
5168
+ const cwd = process.cwd();
5169
+ const { config } = await loadConfig(cwd);
5170
+ return new LocalProvider(config, cwd);
5171
+ }
5172
+
4191
5173
  // src/commands/completion.ts
4192
5174
  var BASH_COMPLETION = `###-begin-pq-completions-###
4193
5175
  _pq_completions() {
@@ -4352,11 +5334,11 @@ function detectShell() {
4352
5334
 
4353
5335
  // src/commands/stop.ts
4354
5336
  import { readFile as readFile5, unlink as unlink2 } from "fs/promises";
4355
- import { resolve as resolve8 } from "path";
5337
+ import { resolve as resolve9 } from "path";
4356
5338
  import { existsSync as existsSync6 } from "fs";
4357
5339
  async function stopCommand(opts) {
4358
5340
  const cwd = process.cwd();
4359
- const serverJsonPath = resolve8(cwd, ".pipequery", "server.json");
5341
+ const serverJsonPath = resolve9(cwd, ".pipequery", "server.json");
4360
5342
  if (!existsSync6(serverJsonPath)) {
4361
5343
  log.error("No running server found.");
4362
5344
  log.dim("Start one with: pq serve or pq serve -d");
@@ -4608,7 +5590,7 @@ program.command("serve").description("Start the PipeQuery server").option("-p, -
4608
5590
  program.command("query <expression>").description("Run an ad-hoc PipeQuery expression").option("-f, --format <format>", "Output format: table, json", "table").action(queryCommand);
4609
5591
  var source = program.command("source").description("Manage data sources");
4610
5592
  source.command("list").description("List configured data sources").action(sourceListCommand);
4611
- source.command("add <name>").description("Add a data source").requiredOption("-t, --type <type>", "Source type: rest, websocket, file").option("-u, --url <url>", "URL for REST or WebSocket sources").option("-i, --interval <interval>", "Poll interval for REST (e.g., 30s, 5m)", "30s").option("-d, --data-path <path>", "Dot-path to extract data from response").option("-p, --path <path>", "File path for file sources").option("-w, --watch", "Watch file for changes").action(sourceAddCommand);
5593
+ source.command("add <name>").description("Add a data source").requiredOption("-t, --type <type>", "Source type: rest, websocket, file, postgres, mysql, sqlite, kafka").option("-u, --url <url>", "Connection URL for REST / WebSocket / Postgres / MySQL sources").option("-i, --interval <interval>", "Poll interval (e.g., 30s, 5m)", "30s").option("-d, --data-path <path>", "Dot-path to extract data from REST response").option("-p, --path <path>", "File path for file or sqlite sources").option("-w, --watch", "Watch file for changes (file sources)").option("-q, --query <sql>", "SELECT query for Postgres / MySQL / SQLite sources").option("--ssl <mode>", "Postgres / MySQL SSL: require (default), no-verify, false; Kafka: true / false").option("--max-rows <n>", "DB sources: safety cap on rows per fetch (default 10000)").option("--no-readonly", "SQLite: open the database read-write instead of read-only").option("-b, --brokers <list>", "Kafka: comma-separated bootstrap brokers (supports ${ENV_VAR})").option("--topic <topic>", "Kafka topic to subscribe to").option("--group-id <id>", "Kafka consumer group (default: ephemeral per-process)").option("--from-beginning", "Kafka: read topic from the start instead of latest").option("--value-format <fmt>", "Kafka value decoding: json (default), string, raw").option("--max-buffer <n>", "Kafka / WebSocket: ring buffer size (default 1000)").action(sourceAddCommand);
4612
5594
  source.command("remove <name>").description("Remove a data source").action(sourceRemoveCommand);
4613
5595
  source.command("test <name>").description("Test a data source and show sample data").action(sourceTestCommand);
4614
5596
  var endpoint = program.command("endpoint").description("Manage API endpoints");
@@ -4621,6 +5603,9 @@ var remote = program.command("remote").description("Manage remote deployment");
4621
5603
  remote.command("deploy").description("Generate Dockerfile and docker-compose.yaml").action(remoteDeployCommand);
4622
5604
  remote.command("connect <url>").description("Connect CLI to a remote PipeQuery server").action(remoteConnectCommand);
4623
5605
  remote.command("status").description("Check remote server health").action(remoteStatusCommand);
5606
+ var mcp = program.command("mcp").description("Model Context Protocol server for AI clients");
5607
+ mcp.command("serve").description("Start an MCP server exposing pipequery as tools (for Claude, Cursor, Copilot, etc.)").option("--http", "Serve over HTTP/SSE instead of stdio").option("-p, --port <port>", "HTTP port (when --http is set)", parseInt).option("-H, --host <host>", "HTTP host (when --http is set)").option("--attach <url>", "Attach to a running `pq serve` instance at the given URL instead of loading pipequery.yaml locally").option("--auth-token <token>", "Require Authorization: Bearer <token> on HTTP requests. Falls back to $PIPEQUERY_MCP_TOKEN. Without either, the endpoint is unauthenticated.").action(mcpServeCommand);
5608
+ mcp.command("inspect").description("Print the MCP tool schemas as JSON (useful for directory submission / debugging)").option("--attach <url>", "Attach to a running `pq serve` instance at the given URL instead of loading pipequery.yaml locally").action(mcpInspectCommand);
4624
5609
  program.command("stop").description("Stop the running PipeQuery server (daemon or foreground)").option("-f, --force", "Force kill (SIGKILL instead of SIGTERM)").action(stopCommand);
4625
5610
  program.command("completion").description("Generate shell completion script").option("-s, --shell <shell>", "Shell type: bash, zsh, fish").action(completionCommand);
4626
5611
  if (process.argv.length <= 2) {