@plaud-ai/mcp 0.3.2 → 0.3.4
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/{chunk-4QBEOJPX.js → chunk-242FRP4P.js} +6 -1
- package/dist/chunk-D2JZW2TW.js +832 -0
- package/dist/{chunk-QJV4OLZI.js → chunk-LOGKF7DL.js} +1 -1
- package/dist/{chunk-QT25MKZJ.js → chunk-VHTXWQUE.js} +34 -27
- package/dist/{chunk-XA77WLYF.js → chunk-XWDA3G2U.js} +15 -2
- package/dist/index.js +14 -15
- package/dist/{install-UUXHA6WE.js → install-ETBOS6SA.js} +65 -21
- package/dist/{server-BOR5EZLF.js → server-NKCNUA6P.js} +0 -1
- package/dist/{server-D3JKIARR.js → server-VVQEILKC.js} +497 -26
- package/dist/{setup-GLWZYNRW.js → setup-SMS45JC5.js} +8 -9
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/dist/chunk-MCKGQKYU.js +0 -15
- package/dist/chunk-R5G6UXSI.js +0 -20687
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
normalizeMcpHost,
|
|
2
3
|
registerTools
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XWDA3G2U.js";
|
|
4
5
|
import {
|
|
5
6
|
PlaudClient
|
|
6
|
-
} from "./chunk-
|
|
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
|
|
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(
|
|
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(
|
|
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.4",
|
|
1235
|
+
buildId: "ce4457b",
|
|
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"] ??
|
|
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"] ??
|
|
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.
|
|
1014
|
-
registerTools(mcpServer, client);
|
|
1476
|
+
const mcpServer = new McpServer({ name: "plaud", version: "0.3.4" });
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
}
|
|
1035
|
-
await mcpServer.close();
|
|
1036
|
-
}
|
|
1507
|
+
});
|
|
1037
1508
|
}
|
|
1038
1509
|
);
|
|
1039
1510
|
if (!process.env.PLAUD_CALLBACK_URL) {
|