@plaud-ai/mcp 0.3.2 → 0.3.3

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.
@@ -1,9 +1,10 @@
1
1
  import {
2
+ normalizeMcpHost,
2
3
  registerTools
3
- } from "./chunk-XA77WLYF.js";
4
+ } from "./chunk-XWDA3G2U.js";
4
5
  import {
5
6
  PlaudClient
6
- } from "./chunk-R5G6UXSI.js";
7
+ } from "./chunk-D2JZW2TW.js";
7
8
  import {
8
9
  logger
9
10
  } from "./chunk-NPCCDRWQ.js";
@@ -12,12 +13,11 @@ import {
12
13
  httpRequestsInProgress,
13
14
  oauthTokenRefresh
14
15
  } from "./chunk-RUFCT6DQ.js";
15
- import "./chunk-MCKGQKYU.js";
16
16
 
17
17
  // src/http/server.ts
18
18
  import express from "express";
19
19
  import { createServer } from "http";
20
- import { randomUUID as randomUUID2 } from "crypto";
20
+ import { randomUUID as randomUUID3 } from "crypto";
21
21
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
22
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
23
  import { createOAuthMetadata, mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
@@ -267,6 +267,15 @@ function validateMetadata(parsed, expectedClientIdUrl) {
267
267
  }
268
268
 
269
269
  // src/http/oauth-provider.ts
270
+ function subFromJwt(token) {
271
+ if (!token) return void 0;
272
+ try {
273
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
274
+ return typeof payload.sub === "string" ? payload.sub : void 0;
275
+ } catch {
276
+ return void 0;
277
+ }
278
+ }
270
279
  var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
271
280
  var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
272
281
  var STATELESS_CODE_PREFIX = "pc1_";
@@ -287,6 +296,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
287
296
  _callbackUrl;
288
297
  _debugOAuthLogs;
289
298
  _cimdLoader;
299
+ _tracker;
290
300
  _registeredClients = /* @__PURE__ */ new Map();
291
301
  // HMAC secret used to sign DCR-issued client_ids so we can recover them
292
302
  // across container restarts without persistent storage. See verifyAndRecover.
@@ -346,6 +356,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
346
356
  this._callbackUrl = options.callbackUrl;
347
357
  this._debugOAuthLogs = options.debugOAuthLogs ?? false;
348
358
  this._cimdLoader = options.cimdLoader;
359
+ this._tracker = options.tracker;
349
360
  const secret = process.env["PLAUD_DCR_HMAC_SECRET"];
350
361
  if (secret && secret.length >= 32) {
351
362
  this._clientIdSecret = Buffer.from(secret, "utf-8");
@@ -380,6 +391,14 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
380
391
  client_id_issued_at: Math.floor(Date.now() / 1e3)
381
392
  };
382
393
  this._registeredClients.set(full.client_id, full);
394
+ this._tracker?.track({
395
+ name: "mcp.dcr_registration_success",
396
+ actorType: "service",
397
+ params: {
398
+ dcr_client_id: full.client_id,
399
+ ...normalizeMcpHost(client.client_name) ? { mcp_host: normalizeMcpHost(client.client_name) } : {}
400
+ }
401
+ });
383
402
  if (this._debugOAuthLogs) {
384
403
  logger.info({
385
404
  event: "oauth_client_registered",
@@ -591,11 +610,45 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
591
610
  });
592
611
  res.redirect(target.toString());
593
612
  }
613
+ /**
614
+ * Resolve the REAL Plaud user_id (the `/users/current` `id` field) from a fresh
615
+ * access token — the same id the CLI/stdio telemetry and the Plaud app use, so
616
+ * warehouse auth.* events join with tool events, subscription and frontend data.
617
+ * The JWT `sub` is only an OAuth pseudo-id (`client_user_…`), so we resolve via
618
+ * the API. Best-effort: on any failure fall back to the JWT sub, so the event
619
+ * still carries a stable id and the auth flow is never blocked.
620
+ */
621
+ async resolveRealUserId(accessToken) {
622
+ if (!accessToken) return void 0;
623
+ try {
624
+ const client = new PlaudClient({
625
+ clientId: this._plaudClientId,
626
+ clientSecret: "",
627
+ redirectUri: "",
628
+ apiBase: this._plaudApiBase,
629
+ staticToken: accessToken
630
+ });
631
+ const user = await client.getCurrentUser();
632
+ const id = typeof user?.id === "string" ? user.id : void 0;
633
+ return id ?? subFromJwt(accessToken);
634
+ } catch {
635
+ return subFromJwt(accessToken);
636
+ }
637
+ }
594
638
  // Override to use PKCE public-client flow — no Basic auth, client_id sent in body.
595
- async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, _redirectUri, resource) {
639
+ async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, _redirectUri, resource) {
640
+ const failedAuth = (failureKind) => {
641
+ this._tracker?.track({
642
+ name: "auth.oauth_callback_error",
643
+ actorType: "user",
644
+ distinctId: client.client_id,
645
+ params: { failure_kind: failureKind }
646
+ });
647
+ };
596
648
  const pendingCode = this.decodeAuthorizationCode(authorizationCode);
597
649
  if (!pendingCode) {
598
650
  logger.warn({ event: "oauth_authorization_code_unknown", code_len: authorizationCode.length });
651
+ failedAuth("business");
599
652
  throw new InvalidGrantError("Unknown or expired authorization code");
600
653
  }
601
654
  if (pendingCode.resource && resource && resource.href !== pendingCode.resource) {
@@ -604,6 +657,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
604
657
  expected_resource: pendingCode.resource,
605
658
  received_resource: resource.href
606
659
  });
660
+ failedAuth("business");
607
661
  throw new InvalidGrantError("Mismatched resource");
608
662
  }
609
663
  const body = {
@@ -641,18 +695,32 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
641
695
  }
642
696
  logger.error({ event: "oauth_token_exchange_failed", status: fetchRes.status, body: text.slice(0, 500) });
643
697
  if (fetchRes.status >= 500) {
698
+ failedAuth("upstream_5xx");
644
699
  throw new McpServerError(`Upstream token endpoint error: ${fetchRes.status} ${text.slice(0, 200)}`);
645
700
  }
701
+ failedAuth("business");
646
702
  throw new InvalidGrantError(`Plaud token exchange: ${fetchRes.status} ${text.slice(0, 200)}`);
647
703
  }
648
704
  let data;
649
705
  try {
650
706
  data = await fetchRes.json();
651
707
  } catch (err) {
708
+ failedAuth("internal");
652
709
  logger.error({ event: "oauth_token_json_parse_failed", error: String(err) });
653
710
  throw new McpServerError("Token endpoint returned non-JSON response");
654
711
  }
655
712
  logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
713
+ const authorizedUserId = await this.resolveRealUserId(data.access_token);
714
+ this._tracker?.track({
715
+ name: "auth.oauth_callback_success",
716
+ actorType: "user",
717
+ userId: authorizedUserId,
718
+ distinctId: authorizedUserId ?? client.client_id,
719
+ params: {
720
+ dcr_client_id: client.client_id,
721
+ ...normalizeMcpHost(client.client_name) ? { mcp_host: normalizeMcpHost(client.client_name) } : {}
722
+ }
723
+ });
656
724
  return {
657
725
  access_token: data.access_token,
658
726
  token_type: data.token_type ?? "Bearer",
@@ -663,7 +731,15 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
663
731
  // Override refresh too: the SDK proxy implementation would forward the local
664
732
  // DCR client_id and RFC 8707 resource to Plaud, but Plaud uses its dedicated
665
733
  // refresh endpoint and does not support resource indicators.
666
- async exchangeRefreshToken(_client, refreshToken, scopes, resource) {
734
+ async exchangeRefreshToken(client, refreshToken, scopes, resource) {
735
+ const failedRefresh = (failureKind) => {
736
+ this._tracker?.track({
737
+ name: "auth.token_refresh_error",
738
+ actorType: "user",
739
+ distinctId: client.client_id,
740
+ params: { failure_kind: failureKind }
741
+ });
742
+ };
667
743
  logger.info({
668
744
  event: "oauth_token_refresh_attempt",
669
745
  token_url: this._plaudRefreshUrl,
@@ -689,9 +765,11 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
689
765
  logger.error({ event: "oauth_token_refresh_failed", status: fetchRes.status, body: text.slice(0, 500) });
690
766
  if (fetchRes.status >= 500) {
691
767
  oauthTokenRefresh.inc({ result: "upstream_error" });
768
+ failedRefresh("upstream_5xx");
692
769
  throw new McpServerError(`Upstream refresh endpoint error: ${fetchRes.status} ${text.slice(0, 200)}`);
693
770
  }
694
771
  oauthTokenRefresh.inc({ result: "invalid_grant" });
772
+ failedRefresh("business");
695
773
  throw new InvalidGrantError(`Plaud token refresh: ${fetchRes.status} ${text.slice(0, 200)}`);
696
774
  }
697
775
  let data;
@@ -699,10 +777,18 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
699
777
  data = await fetchRes.json();
700
778
  } catch (err) {
701
779
  oauthTokenRefresh.inc({ result: "parse_failed" });
780
+ failedRefresh("internal");
702
781
  logger.error({ event: "oauth_refresh_json_parse_failed", error: String(err) });
703
782
  throw new McpServerError("Refresh endpoint returned non-JSON response");
704
783
  }
705
784
  oauthTokenRefresh.inc({ result: "success" });
785
+ const refreshedUserId = await this.resolveRealUserId(data.access_token);
786
+ this._tracker?.track({
787
+ name: "auth.token_refresh_success",
788
+ actorType: "user",
789
+ userId: refreshedUserId,
790
+ distinctId: refreshedUserId ?? client.client_id
791
+ });
706
792
  logger.info({
707
793
  event: "oauth_token_refresh_ok",
708
794
  has_refresh_token: !!data.refresh_token,
@@ -718,7 +804,360 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
718
804
  }
719
805
  };
720
806
 
807
+ // src/telemetry-server/tracker.ts
808
+ import { randomUUID as randomUUID2 } from "crypto";
809
+
810
+ // src/telemetry-server/transport.ts
811
+ import { PostHog } from "posthog-node";
812
+ var DryRunTransport = class {
813
+ name = "dry-run";
814
+ async send(batch) {
815
+ for (const ev of batch) {
816
+ logger.info({
817
+ event: "warehouse_dryrun",
818
+ emitted_event: ev.event,
819
+ distinct_id: ev.distinct_id,
820
+ payload: ev
821
+ });
822
+ }
823
+ }
824
+ };
825
+ var PostHogTransport = class {
826
+ name = "posthog-node";
827
+ client;
828
+ constructor(endpoint, apiKey) {
829
+ this.client = new PostHog(apiKey, {
830
+ host: endpoint,
831
+ flushAt: 5,
832
+ flushInterval: 1e3,
833
+ // Self-verification hook: posthog-node's capture() is silent on success,
834
+ // so we wrap its fetch to log the warehouse's HTTP response code on every
835
+ // ingestion request. A 2xx here is the warehouse's own ACK that the batch
836
+ // was received + accepted (auth ok, payload valid) — the only local proof,
837
+ // short of reading the warehouse, that events actually reach it. Low volume
838
+ // (one line per flush). Does NOT alter posthog-node's wire path.
839
+ fetch: async (url, options) => {
840
+ try {
841
+ const res = await fetch(url, options);
842
+ logger.info({
843
+ event: "warehouse_http_response",
844
+ status: res.status,
845
+ ok: res.ok,
846
+ url
847
+ });
848
+ return res;
849
+ } catch (err) {
850
+ logger.warn({ event: "warehouse_http_error", url, error: String(err) });
851
+ throw err;
852
+ }
853
+ }
854
+ });
855
+ }
856
+ async send(batch) {
857
+ for (const ev of batch) {
858
+ this.client.capture({
859
+ distinctId: ev.distinct_id,
860
+ event: ev.event,
861
+ properties: ev.properties,
862
+ timestamp: new Date(ev.timestamp),
863
+ disableGeoip: true
864
+ // server-side; don't derive geo from the server IP
865
+ });
866
+ }
867
+ }
868
+ async shutdown() {
869
+ await this.client.shutdown();
870
+ }
871
+ };
872
+
873
+ // src/telemetry-server/tracker.ts
874
+ var EVENT_NAME_RE = /^[a-z]+\.[a-z][a-z0-9_]{2,58}$/;
875
+ var PARAM_KEY_RE = /^[a-z][a-z0-9_]*$/;
876
+ var MAX_BATCH_BYTES = 15 * 1024;
877
+ var WarehouseTracker = class {
878
+ common;
879
+ transport;
880
+ batchSize;
881
+ flushIntervalMs;
882
+ queue = [];
883
+ timer = null;
884
+ closed = false;
885
+ constructor(opts) {
886
+ this.common = opts.common;
887
+ this.transport = opts.transport ?? new DryRunTransport();
888
+ this.batchSize = opts.batchSize ?? 5;
889
+ this.flushIntervalMs = opts.flushIntervalMs ?? 1e3;
890
+ }
891
+ /**
892
+ * Fire-and-forget. Assembles + enqueues one event. Never throws — any error
893
+ * is logged and swallowed so the HTTP request path is never affected.
894
+ */
895
+ track(input) {
896
+ if (this.closed) return;
897
+ try {
898
+ const event = this.assemble(input);
899
+ this.queue.push(event);
900
+ if (this.queue.length >= this.batchSize) {
901
+ void this.flush();
902
+ } else {
903
+ this.ensureTimer();
904
+ }
905
+ } catch (err) {
906
+ logger.warn({ event: "warehouse_track_dropped", name: input.name, error: String(err) });
907
+ }
908
+ }
909
+ /** Send everything currently queued. Safe to call anytime. */
910
+ async flush() {
911
+ if (this.timer) {
912
+ clearTimeout(this.timer);
913
+ this.timer = null;
914
+ }
915
+ if (this.queue.length === 0) return;
916
+ const pending = this.queue;
917
+ this.queue = [];
918
+ for (const batch of this.splitBySize(pending)) {
919
+ try {
920
+ await this.transport.send(batch);
921
+ } catch (err) {
922
+ logger.warn({ event: "warehouse_flush_failed", count: batch.length, error: String(err) });
923
+ }
924
+ }
925
+ }
926
+ /** Flush remaining events and stop. Call before process exit. */
927
+ async shutdown() {
928
+ this.closed = true;
929
+ await this.flush();
930
+ try {
931
+ await this.transport.shutdown?.();
932
+ } catch (err) {
933
+ logger.warn({ event: "warehouse_transport_shutdown_failed", error: String(err) });
934
+ }
935
+ }
936
+ // ── internals ──────────────────────────────────────────────────────────
937
+ ensureTimer() {
938
+ if (this.timer) return;
939
+ this.timer = setTimeout(() => {
940
+ this.timer = null;
941
+ void this.flush();
942
+ }, this.flushIntervalMs);
943
+ this.timer.unref?.();
944
+ }
945
+ /** Build the full SOP envelope for one event. Throws on invalid input. */
946
+ assemble(input) {
947
+ if (!EVENT_NAME_RE.test(input.name)) {
948
+ throw new Error(`invalid event name (need domain.event_phrase): ${input.name}`);
949
+ }
950
+ const distinctId = this.resolveDistinctId(input);
951
+ if (!distinctId) {
952
+ throw new Error(`empty distinct_id for ${input.name}`);
953
+ }
954
+ const ws = input.workspace ?? {};
955
+ const properties = {
956
+ // identity
957
+ actor_type: input.actorType,
958
+ ...input.userId ? { user_id: input.userId } : {},
959
+ ...ws.id ?? this.common.workspaceId ? { workspace_id: ws.id ?? this.common.workspaceId } : {},
960
+ ...ws.type ?? this.common.workspaceType ? { workspace_type: ws.type ?? this.common.workspaceType } : {},
961
+ ...ws.memberId ?? this.common.memberId ? { member_id: ws.memberId ?? this.common.memberId } : {},
962
+ // service / platform / env
963
+ region: this.common.region,
964
+ platform: "server",
965
+ env: this.common.env,
966
+ service_name: this.common.serviceName,
967
+ service_version: this.common.serviceVersion,
968
+ ...this.common.buildId ? { build_id: this.common.buildId } : {},
969
+ // timing / tracing
970
+ event_id: randomUUID2(),
971
+ ...input.requestId ? { request_id: input.requestId } : {},
972
+ // (trace_id/span_id omitted — no OpenTelemetry; spec forbids fabricating)
973
+ // non-user actors: don't create a PostHog person
974
+ ...input.actorType !== "user" ? { $process_person_profile: false } : {},
975
+ // event-specific custom params
976
+ ...this.validatedParams(input.params)
977
+ };
978
+ return {
979
+ event: input.name,
980
+ distinct_id: distinctId,
981
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
982
+ properties
983
+ };
984
+ }
985
+ /** §3.1 distinct_id rules. */
986
+ resolveDistinctId(input) {
987
+ if (input.distinctId) return input.distinctId;
988
+ if (input.actorType === "user") {
989
+ if (!input.userId) throw new Error(`actor_type=user requires userId: ${input.name}`);
990
+ return input.userId;
991
+ }
992
+ return this.common.serviceName;
993
+ }
994
+ /** Runtime field-name lint only (§4.2): snake_case, no `$`, no nested objects. */
995
+ validatedParams(params) {
996
+ if (!params) return {};
997
+ const out = {};
998
+ for (const [key, value] of Object.entries(params)) {
999
+ if (!PARAM_KEY_RE.test(key)) throw new Error(`invalid param key (snake_case, no $): ${key}`);
1000
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1001
+ throw new Error(`nested object not allowed in param: ${key}`);
1002
+ }
1003
+ out[key] = value;
1004
+ }
1005
+ return out;
1006
+ }
1007
+ /**
1008
+ * Split a list of events into batches each < MAX_BATCH_BYTES. Normally the
1009
+ * batchSize cap (5) keeps us well under 16 KiB; this is the hard safety net.
1010
+ * A single event that alone exceeds the limit is sent on its own with a warn.
1011
+ */
1012
+ splitBySize(events) {
1013
+ const batches = [];
1014
+ let current = [];
1015
+ for (const ev of events) {
1016
+ const evBytes = Buffer.byteLength(JSON.stringify(ev), "utf8");
1017
+ if (evBytes >= MAX_BATCH_BYTES) {
1018
+ if (current.length) {
1019
+ batches.push(current);
1020
+ current = [];
1021
+ }
1022
+ logger.warn({ event: "warehouse_event_oversized", emitted_event: ev.event, bytes: evBytes });
1023
+ batches.push([ev]);
1024
+ continue;
1025
+ }
1026
+ const tentative = [...current, ev];
1027
+ const bytes = Buffer.byteLength(JSON.stringify(tentative), "utf8");
1028
+ if (bytes >= MAX_BATCH_BYTES) {
1029
+ batches.push(current);
1030
+ current = [ev];
1031
+ } else {
1032
+ current = tentative;
1033
+ }
1034
+ }
1035
+ if (current.length) batches.push(current);
1036
+ return batches;
1037
+ }
1038
+ };
1039
+
1040
+ // src/telemetry-server/context.ts
1041
+ import { AsyncLocalStorage } from "async_hooks";
1042
+ var storage = new AsyncLocalStorage();
1043
+ function runWithTelemetryContext(ctx, fn) {
1044
+ return storage.run(ctx, fn);
1045
+ }
1046
+ function getTelemetryContext() {
1047
+ return storage.getStore() ?? {};
1048
+ }
1049
+
1050
+ // src/telemetry-server/hooks.ts
1051
+ function createWarehouseToolHooks(tracker) {
1052
+ const baseInput = (tool, fileId) => {
1053
+ const ctx = getTelemetryContext();
1054
+ return {
1055
+ actorType: "user",
1056
+ userId: ctx.userId,
1057
+ requestId: ctx.requestId,
1058
+ workspace: ctx.workspace,
1059
+ params: {
1060
+ tool_name: tool,
1061
+ transport: "http",
1062
+ ...fileId ? { file_id: fileId } : {},
1063
+ ...ctx.mcpHost ? { mcp_host: ctx.mcpHost } : {}
1064
+ }
1065
+ };
1066
+ };
1067
+ return {
1068
+ onToolClick({ tool, fileId }) {
1069
+ tracker.track({ name: "mcp.tool_click", ...baseInput(tool, fileId) });
1070
+ },
1071
+ onToolResult({ tool, status, durationMs, fileId, failureKind }) {
1072
+ const base = baseInput(tool, fileId);
1073
+ base.params.duration_ms = durationMs;
1074
+ if (status === "error" && failureKind) base.params.failure_kind = failureKind;
1075
+ tracker.track({
1076
+ name: status === "success" ? "mcp.tool_success" : "mcp.tool_error",
1077
+ ...base
1078
+ });
1079
+ }
1080
+ };
1081
+ }
1082
+
1083
+ // src/telemetry-server/connectivity.ts
1084
+ import dns from "dns/promises";
1085
+ import net from "net";
1086
+ var TIMEOUT_MS = 8e3;
1087
+ var DEFAULT_HOSTS = [
1088
+ "plaud-data-warehouse-staging-usw2-lan.plaud.ai",
1089
+ // staging (David's "test env")
1090
+ "plaud-data-warehouse-usw2-lan.plaud.ai"
1091
+ // production US
1092
+ ];
1093
+ function tcpConnect(host) {
1094
+ return new Promise((resolve) => {
1095
+ const socket = net.connect({ host, port: 443 });
1096
+ const done = (r) => {
1097
+ socket.destroy();
1098
+ resolve(r);
1099
+ };
1100
+ socket.setTimeout(TIMEOUT_MS);
1101
+ socket.once("connect", () => done({ ok: true }));
1102
+ socket.once("timeout", () => done({ ok: false, code: "ETIMEDOUT" }));
1103
+ socket.once("error", (err) => done({ ok: false, code: err.code ?? "ERR" }));
1104
+ });
1105
+ }
1106
+ async function probeHost(host) {
1107
+ let ips = null;
1108
+ try {
1109
+ ips = (await dns.lookup(host, { all: true })).map((x) => x.address);
1110
+ } catch (err) {
1111
+ logger.info({
1112
+ event: "warehouse_connectivity",
1113
+ host,
1114
+ reachable: false,
1115
+ stage: "dns",
1116
+ code: err.code,
1117
+ verdict: "DNS failed \u2192 internal DNS not resolvable here \u2192 ops: configure DNS"
1118
+ });
1119
+ return;
1120
+ }
1121
+ const tcp = await tcpConnect(host);
1122
+ logger.info({
1123
+ event: "warehouse_connectivity",
1124
+ host,
1125
+ reachable: tcp.ok,
1126
+ stage: tcp.ok ? "tcp_ok" : "tcp",
1127
+ dns_ips: ips,
1128
+ code: tcp.code,
1129
+ verdict: tcp.ok ? "TCP :443 connected \u2192 network path open \u2192 remaining is key/app layer (David)" : `TCP failed (${tcp.code}) \u2192 route/firewall blocked \u2192 ops: allow egress to *.lan:443`
1130
+ });
1131
+ }
1132
+ function runWarehouseConnectivityCheck(configuredEndpoint) {
1133
+ const hosts = new Set(DEFAULT_HOSTS);
1134
+ if (configuredEndpoint) {
1135
+ try {
1136
+ hosts.add(new URL(configuredEndpoint).hostname);
1137
+ } catch {
1138
+ }
1139
+ }
1140
+ void (async () => {
1141
+ logger.info({ event: "warehouse_connectivity_start", hosts: [...hosts] });
1142
+ for (const h of hosts) {
1143
+ try {
1144
+ await probeHost(h);
1145
+ } catch (err) {
1146
+ logger.warn({ event: "warehouse_connectivity_error", host: h, error: String(err) });
1147
+ }
1148
+ }
1149
+ })();
1150
+ }
1151
+
721
1152
  // src/http/server.ts
1153
+ function clientNameFromBody(body) {
1154
+ const msgs = Array.isArray(body) ? body : [body];
1155
+ for (const m of msgs) {
1156
+ const name = m?.params?.clientInfo?.name;
1157
+ if (typeof name === "string" && name) return name;
1158
+ }
1159
+ return void 0;
1160
+ }
722
1161
  var HTTP_PORT = Number(process.env.PLAUD_HTTP_PORT ?? 3e3);
723
1162
  var HTTP_HOST = process.env.PLAUD_HTTP_HOST ?? "0.0.0.0";
724
1163
  var CALLBACK_PORT = 8199;
@@ -786,6 +1225,28 @@ function startHttpServer() {
786
1225
  allow_private_ip: CIMD_ALLOW_PRIVATE_IP
787
1226
  });
788
1227
  }
1228
+ const warehouseEndpoint = process.env.PLAUD_WAREHOUSE_ENDPOINT;
1229
+ const warehouseApiKey = process.env.PLAUD_WAREHOUSE_API_KEY;
1230
+ const warehouse = new WarehouseTracker({
1231
+ common: {
1232
+ serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
1233
+ // matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
1234
+ serviceVersion: "0.3.3",
1235
+ buildId: "76e2306",
1236
+ // mcp tsup TODO: inject git short SHA (like CLI)
1237
+ region: process.env.PLAUD_REGION ?? "US",
1238
+ env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
1239
+ },
1240
+ transport: warehouseEndpoint && warehouseApiKey ? new PostHogTransport(warehouseEndpoint, warehouseApiKey) : new DryRunTransport()
1241
+ });
1242
+ const warehouseToolHooks = createWarehouseToolHooks(warehouse);
1243
+ logger.info({ event: "warehouse_emitter_ready", transport: warehouseEndpoint && warehouseApiKey ? "posthog-node" : "dry-run" });
1244
+ runWarehouseConnectivityCheck(warehouseEndpoint);
1245
+ const shutdownWarehouse = () => {
1246
+ void warehouse.shutdown();
1247
+ };
1248
+ process.once("SIGTERM", shutdownWarehouse);
1249
+ process.once("SIGINT", shutdownWarehouse);
789
1250
  const provider = new PlaudOAuthProvider({
790
1251
  clientId,
791
1252
  clientSecret,
@@ -795,7 +1256,9 @@ function startHttpServer() {
795
1256
  refreshUrl: process.env.PLAUD_REFRESH_URL,
796
1257
  apiBase,
797
1258
  debugOAuthLogs: OAUTH_DEBUG_LOGS,
798
- cimdLoader
1259
+ cimdLoader,
1260
+ tracker: warehouse
1261
+ // auth.* events
799
1262
  });
800
1263
  const issuerUrl = new URL(serverUrl);
801
1264
  const trimmedServerUrl = serverUrl.replace(/\/$/, "");
@@ -875,7 +1338,7 @@ function startHttpServer() {
875
1338
  });
876
1339
  }
877
1340
  app.use((req, res, next) => {
878
- const reqId = req.headers["x-request-id"] ?? randomUUID2();
1341
+ const reqId = req.headers["x-request-id"] ?? randomUUID3();
879
1342
  const startMs = Date.now();
880
1343
  res.locals["reqId"] = reqId;
881
1344
  res.on("finish", () => {
@@ -999,7 +1462,7 @@ function startHttpServer() {
999
1462
  requireBearerAuth({ verifier: provider, resourceMetadataUrl: protectedResourceMetadataUrl }),
1000
1463
  async (req, res) => {
1001
1464
  const token = req.auth.token;
1002
- const reqId = res.locals["reqId"] ?? randomUUID2();
1465
+ const reqId = res.locals["reqId"] ?? randomUUID3();
1003
1466
  const reqLog = logger.child({ req_id: reqId });
1004
1467
  const startMs = Date.now();
1005
1468
  reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
@@ -1010,30 +1473,38 @@ function startHttpServer() {
1010
1473
  apiBase,
1011
1474
  staticToken: token
1012
1475
  });
1013
- const mcpServer = new McpServer({ name: "plaud", version: "0.3.2" });
1014
- registerTools(mcpServer, client);
1476
+ const mcpServer = new McpServer({ name: "plaud", version: "0.3.3" });
1477
+ registerTools(mcpServer, client, warehouseToolHooks);
1015
1478
  const transport = new StreamableHTTPServerTransport({
1016
1479
  sessionIdGenerator: void 0,
1017
1480
  // stateless
1018
1481
  enableDnsRebindingProtection: true,
1019
1482
  allowedOrigins: ALLOWED_ORIGINS
1020
1483
  });
1021
- try {
1022
- await mcpServer.connect(transport);
1023
- await transport.handleRequest(req, res, req.body);
1024
- const clientVersion = mcpServer.server.getClientVersion();
1025
- if (clientVersion) {
1026
- reqLog.info({ event: "mcp_client_info", client_name: clientVersion.name, client_version: clientVersion.version });
1027
- }
1028
- reqLog.info({ event: "mcp_request_end", duration_ms: Date.now() - startMs });
1029
- } catch (err) {
1030
- reqLog.error({ event: "mcp_request_error", error: String(err), duration_ms: Date.now() - startMs });
1031
- if (!res.headersSent) {
1032
- res.status(500).json({ error: String(err) });
1484
+ const ctxUserId = req.auth.clientId && req.auth.clientId !== "unknown" ? req.auth.clientId : void 0;
1485
+ const telemetryCtx = {
1486
+ userId: ctxUserId,
1487
+ requestId: reqId,
1488
+ mcpHost: normalizeMcpHost(clientNameFromBody(req.body))
1489
+ };
1490
+ await runWithTelemetryContext(telemetryCtx, async () => {
1491
+ try {
1492
+ await mcpServer.connect(transport);
1493
+ await transport.handleRequest(req, res, req.body);
1494
+ const clientVersion = mcpServer.server.getClientVersion();
1495
+ if (clientVersion) {
1496
+ reqLog.info({ event: "mcp_client_info", client_name: clientVersion.name, client_version: clientVersion.version });
1497
+ }
1498
+ reqLog.info({ event: "mcp_request_end", duration_ms: Date.now() - startMs });
1499
+ } catch (err) {
1500
+ reqLog.error({ event: "mcp_request_error", error: String(err), duration_ms: Date.now() - startMs });
1501
+ if (!res.headersSent) {
1502
+ res.status(500).json({ error: String(err) });
1503
+ }
1504
+ } finally {
1505
+ await mcpServer.close();
1033
1506
  }
1034
- } finally {
1035
- await mcpServer.close();
1036
- }
1507
+ });
1037
1508
  }
1038
1509
  );
1039
1510
  if (!process.env.PLAUD_CALLBACK_URL) {
@@ -4,7 +4,6 @@ import {
4
4
  import {
5
5
  registry
6
6
  } from "./chunk-RUFCT6DQ.js";
7
- import "./chunk-MCKGQKYU.js";
8
7
 
9
8
  // src/metrics/server.ts
10
9
  import express from "express";