@plaud-ai/mcp 0.3.4 → 0.3.6

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,7 +1,7 @@
1
1
  import {
2
2
  PlaudClient,
3
3
  capture
4
- } from "./chunk-D2JZW2TW.js";
4
+ } from "./chunk-OYI4PXYP.js";
5
5
  import {
6
6
  httpClientDuration,
7
7
  httpClientRequests
@@ -82,6 +82,16 @@ var TokenStore = class {
82
82
  };
83
83
 
84
84
  // ../shared/dist/oauth.js
85
+ function clientUserIdFromAccessToken(token) {
86
+ if (!token)
87
+ return void 0;
88
+ try {
89
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
90
+ return typeof payload.sub === "string" && payload.sub.length > 0 ? payload.sub : void 0;
91
+ } catch {
92
+ return void 0;
93
+ }
94
+ }
85
95
  var DEFAULT_AUTHORIZATION_URL = "https://web.plaud.ai/platform/oauth";
86
96
  var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
87
97
  var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
@@ -419,6 +429,23 @@ function runOAuthCallback(opts) {
419
429
  });
420
430
  }
421
431
 
432
+ // ../shared/dist/source-block.js
433
+ async function loadBlockContent(block) {
434
+ if (!block)
435
+ return "";
436
+ const inline = block.data_content;
437
+ if (typeof inline === "string" && inline.length > 0)
438
+ return inline;
439
+ const link = block.data_link;
440
+ if (typeof link === "string" && link.length > 0) {
441
+ const res = await fetch(link);
442
+ if (!res.ok)
443
+ throw new Error(`Failed to fetch block content from data_link (HTTP ${res.status})`);
444
+ return await res.text();
445
+ }
446
+ return "";
447
+ }
448
+
422
449
  // ../telemetry/dist/client.js
423
450
  import { PostHog } from "posthog-node";
424
451
 
@@ -497,6 +524,8 @@ var EMAIL_REGEX = /[\w.+-]+@[\w-]+\.[\w.-]+/;
497
524
  var ALLOWED_PROPERTIES = /* @__PURE__ */ new Set([
498
525
  // Global common properties (Plaud Common Event Properties + Spec §2)
499
526
  "user_id",
527
+ "client_user_id",
528
+ // per-OAuth-client user id (JWT sub, `client_user_…`); DA common param across web/CLI/stdio/HTTP (Mirela 2026-06-29)
500
529
  "member_id",
501
530
  // fill-if-present; OAuth /users/current doesn't return it yet (Q16)
502
531
  "workspace_id",
@@ -696,6 +725,7 @@ var state = {
696
725
  memberId: null,
697
726
  workspaceId: null,
698
727
  role: null,
728
+ clientUserId: null,
699
729
  mcpHost: null
700
730
  };
701
731
  async function initTelemetry(options) {
@@ -716,6 +746,7 @@ async function initTelemetry(options) {
716
746
  state.memberId = identity?.memberId ?? null;
717
747
  state.workspaceId = identity?.workspaceId ?? null;
718
748
  state.role = identity?.role ?? null;
749
+ state.clientUserId = identity?.clientUserId ?? null;
719
750
  state.currentDistinctId = identity?.idHash || userId || await getAnonymousId();
720
751
  state.initialised = true;
721
752
  }
@@ -727,6 +758,7 @@ async function setUser(userId, identity) {
727
758
  state.memberId = identity?.memberId ?? null;
728
759
  state.workspaceId = identity?.workspaceId ?? null;
729
760
  state.role = identity?.role ?? null;
761
+ state.clientUserId = identity?.clientUserId ?? null;
730
762
  if (state.surface) {
731
763
  try {
732
764
  await saveUserIdentity(state.surface, userId, identity);
@@ -752,6 +784,7 @@ async function clearUser() {
752
784
  state.memberId = null;
753
785
  state.workspaceId = null;
754
786
  state.role = null;
787
+ state.clientUserId = null;
755
788
  if (state.surface) {
756
789
  try {
757
790
  await clearUserIdentity(state.surface);
@@ -797,6 +830,8 @@ function buildCommonProperties() {
797
830
  out.transport = state.transport;
798
831
  if (state.currentUserId)
799
832
  out.user_id = state.currentUserId;
833
+ if (state.clientUserId)
834
+ out.client_user_id = state.clientUserId;
800
835
  if (state.memberId)
801
836
  out.member_id = state.memberId;
802
837
  if (state.workspaceId)
@@ -820,8 +855,10 @@ function extractIdentity(user) {
820
855
  export {
821
856
  classifyError,
822
857
  oauthCallbackErrorType,
858
+ clientUserIdFromAccessToken,
823
859
  PlaudClient,
824
860
  runOAuthCallback,
861
+ loadBlockContent,
825
862
  shutdown,
826
863
  initTelemetry,
827
864
  setUser,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  capture,
3
- classifyError
4
- } from "./chunk-D2JZW2TW.js";
3
+ classifyError,
4
+ loadBlockContent
5
+ } from "./chunk-OYI4PXYP.js";
5
6
  import {
6
7
  logger
7
8
  } from "./chunk-NPCCDRWQ.js";
@@ -16,6 +17,22 @@ import { randomUUID } from "crypto";
16
17
  var toolHooks = null;
17
18
  var MAX_FILTER_PAGES = 5;
18
19
  var FILTER_PAGE_SIZE = 100;
20
+ var TRANSCRIPT_PAGE_SIZE = 50;
21
+ var TRANSCRIPT_BLOCKS = ["transaction", "outline", "transaction_polish"];
22
+ function encodeTranscriptCursor(offset) {
23
+ return Buffer.from(JSON.stringify({ o: offset }), "utf8").toString("base64url");
24
+ }
25
+ function decodeTranscriptCursor(cursor) {
26
+ try {
27
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
28
+ if (typeof parsed?.o === "number" && Number.isInteger(parsed.o) && parsed.o >= 0) {
29
+ return parsed.o;
30
+ }
31
+ return null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
19
36
  function parseDate(s) {
20
37
  if (!s) return null;
21
38
  const d = new Date(s);
@@ -198,24 +215,90 @@ function registerTools(server, client, hooks) {
198
215
  server.registerTool(
199
216
  "get_transcript",
200
217
  {
201
- description: "Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
218
+ description: "Fetch the timestamped transcript with speaker attribution for a Plaud recording. Defaults to the `transaction` block (real speaker names + timestamps), returned one page of utterances at a time to stay within client size limits \u2014 call again with the returned `next_cursor` to fetch the next page. Set `block` to `outline` or `transaction_polish` to fetch those blocks instead.",
202
219
  annotations: {
203
220
  title: "Get recording transcript",
204
221
  readOnlyHint: true,
205
222
  destructiveHint: false,
206
223
  openWorldHint: true
207
224
  },
208
- inputSchema: { file_id: z.string().describe("The file ID to retrieve transcript for") }
225
+ inputSchema: {
226
+ file_id: z.string().describe("The file ID to retrieve transcript for"),
227
+ block: z.enum(TRANSCRIPT_BLOCKS).optional().describe(
228
+ "Which source block to fetch: `transaction` (default; raw transcript, speaker + timestamps), `transaction_polish` (AI-cleaned transcript; same per-utterance shape, keeps speaker + timestamps), or `outline`."
229
+ ),
230
+ cursor: z.string().optional().describe("Opaque pagination cursor from a previous call's `next_cursor`. Omit to start from the first utterance."),
231
+ limit: z.number().int().min(1).max(500).optional().describe(`Max utterances to return in this page (default ${TRANSCRIPT_PAGE_SIZE}). Only applies to blocks returned as an utterance list.`)
232
+ }
209
233
  },
210
- async ({ file_id }) => {
234
+ async ({ file_id, block, cursor, limit }) => {
211
235
  const start = Date.now();
212
- logger.info({ event: "tool_call", tool: "get_transcript", file_id });
236
+ const blockType = block ?? "transaction";
237
+ logger.info({ event: "tool_call", tool: "get_transcript", file_id, block: blockType });
213
238
  const requestId = emitToolClick("get_transcript", { file_id });
214
239
  try {
215
240
  const file = await client.getFile(file_id);
241
+ const sourceList = file.source_list ?? [];
242
+ if (sourceList.length === 0) {
243
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
244
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, paginated: false });
245
+ return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
246
+ }
247
+ const selected = sourceList.find((s) => s.data_type === blockType);
248
+ if (!selected) {
249
+ const available = sourceList.map((s) => s.data_type).filter(Boolean).join(", ") || "(none)";
250
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
251
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, found: false });
252
+ return {
253
+ content: [{ type: "text", text: `Block "${blockType}" not available for this recording. Available blocks: ${available}.` }]
254
+ };
255
+ }
256
+ const content = await loadBlockContent(selected);
257
+ let segments = null;
258
+ if (content) {
259
+ try {
260
+ const parsed = JSON.parse(content);
261
+ if (Array.isArray(parsed)) segments = parsed;
262
+ } catch {
263
+ segments = null;
264
+ }
265
+ }
266
+ if (!segments) {
267
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
268
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, paginated: false });
269
+ const text = content || `Block "${blockType}" has no content for this recording yet.`;
270
+ return { content: [{ type: "text", text }] };
271
+ }
272
+ let offset = 0;
273
+ if (cursor !== void 0) {
274
+ const decoded = decodeTranscriptCursor(cursor);
275
+ if (decoded === null) {
276
+ recordToolMetric("get_transcript", "error", Date.now() - start, requestId, { file_id }, new Error("invalid_cursor"));
277
+ logger.warn({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, error: "invalid_cursor" });
278
+ return {
279
+ content: [{ type: "text", text: "Invalid cursor. Omit `cursor` to start from the first utterance." }],
280
+ isError: true
281
+ };
282
+ }
283
+ offset = decoded;
284
+ }
285
+ const pageSize = limit ?? TRANSCRIPT_PAGE_SIZE;
286
+ const page = segments.slice(offset, offset + pageSize);
287
+ const nextOffset = offset + page.length;
288
+ const hasMore = nextOffset < segments.length;
289
+ const payload = {
290
+ file_id,
291
+ block: blockType,
292
+ total: segments.length,
293
+ offset,
294
+ limit: pageSize,
295
+ returned: page.length,
296
+ next_cursor: hasMore ? encodeTranscriptCursor(nextOffset) : null,
297
+ segments: page
298
+ };
216
299
  recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
217
- logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start });
218
- return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
300
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, paginated: true, total: segments.length, offset, returned: page.length });
301
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
219
302
  } catch (err) {
220
303
  recordToolMetric("get_transcript", "error", Date.now() - start, requestId, { file_id }, err);
221
304
  logger.error({ event: "tool_call_error", tool: "get_transcript", duration_ms: Date.now() - start, error: String(err) });
package/dist/index.js CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient
4
- } from "./chunk-LOGKF7DL.js";
4
+ } from "./chunk-3K3M2X74.js";
5
5
  import {
6
6
  loadSkills
7
7
  } from "./chunk-242FRP4P.js";
8
8
  import {
9
9
  normalizeMcpHost,
10
10
  registerTools
11
- } from "./chunk-XWDA3G2U.js";
11
+ } from "./chunk-YOMRLHNX.js";
12
12
  import {
13
13
  capture,
14
14
  classifyError,
15
15
  clearUser,
16
+ clientUserIdFromAccessToken,
16
17
  extractIdentity,
17
18
  initTelemetry,
18
19
  oauthCallbackErrorType,
@@ -20,7 +21,7 @@ import {
20
21
  setMcpHost,
21
22
  setUser,
22
23
  shutdown
23
- } from "./chunk-D2JZW2TW.js";
24
+ } from "./chunk-OYI4PXYP.js";
24
25
  import "./chunk-NPCCDRWQ.js";
25
26
  import "./chunk-RUFCT6DQ.js";
26
27
 
@@ -30,7 +31,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
30
31
  import open from "open";
31
32
  var server = new McpServer({
32
33
  name: "plaud",
33
- version: "0.3.4"
34
+ version: "0.3.6"
34
35
  });
35
36
  var CALLBACK_PORT = 8199;
36
37
  var LOGIN_TIMEOUT_MS = 12e4;
@@ -81,7 +82,11 @@ server.registerTool("login", {
81
82
  try {
82
83
  const user = await client.getCurrentUser();
83
84
  if (user && typeof user.id === "string") {
84
- await setUser(user.id, extractIdentity(user));
85
+ const token = await client.auth.getAccessToken();
86
+ await setUser(user.id, {
87
+ ...extractIdentity(user),
88
+ clientUserId: clientUserIdFromAccessToken(token)
89
+ });
85
90
  }
86
91
  } catch {
87
92
  }
@@ -153,7 +158,7 @@ async function main() {
153
158
  const sub = process.argv[2];
154
159
  const sub2 = process.argv[3];
155
160
  if (sub === "install") {
156
- const { runInstall } = await import("./install-ETBOS6SA.js");
161
+ const { runInstall } = await import("./install-3E63G6QX.js");
157
162
  const args = process.argv.slice(3);
158
163
  const yes = args.some((a) => a === "--yes" || a === "-y");
159
164
  const noLogin = args.some((a) => a === "--no-login");
@@ -186,7 +191,7 @@ async function main() {
186
191
  return;
187
192
  }
188
193
  if (sub === "http") {
189
- const { startHttpServer } = await import("./server-VVQEILKC.js");
194
+ const { startHttpServer } = await import("./server-PJTOHPPJ.js");
190
195
  const { startMetricsServer } = await import("./server-NKCNUA6P.js");
191
196
  startMetricsServer();
192
197
  startHttpServer();
@@ -212,7 +217,7 @@ Usage:
212
217
  try {
213
218
  await initTelemetry({
214
219
  surface: "mcp",
215
- appVersion: "0.3.4",
220
+ appVersion: "0.3.6",
216
221
  transport: "stdio"
217
222
  });
218
223
  } catch {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-LOGKF7DL.js";
3
+ } from "./chunk-3K3M2X74.js";
4
4
  import {
5
5
  commandPathIsStale,
6
6
  copyToClipboard,
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-242FRP4P.js";
13
13
  import {
14
14
  runOAuthCallback
15
- } from "./chunk-D2JZW2TW.js";
15
+ } from "./chunk-OYI4PXYP.js";
16
16
  import "./chunk-RUFCT6DQ.js";
17
17
 
18
18
  // src/install.ts
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  normalizeMcpHost,
3
3
  registerTools
4
- } from "./chunk-XWDA3G2U.js";
4
+ } from "./chunk-YOMRLHNX.js";
5
5
  import {
6
6
  PlaudClient
7
- } from "./chunk-D2JZW2TW.js";
7
+ } from "./chunk-OYI4PXYP.js";
8
8
  import {
9
9
  logger
10
10
  } from "./chunk-NPCCDRWQ.js";
@@ -610,31 +610,6 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
610
610
  });
611
611
  res.redirect(target.toString());
612
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
- }
638
613
  // Override to use PKCE public-client flow — no Basic auth, client_id sent in body.
639
614
  async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, _redirectUri, resource) {
640
615
  const failedAuth = (failureKind) => {
@@ -710,7 +685,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
710
685
  throw new McpServerError("Token endpoint returned non-JSON response");
711
686
  }
712
687
  logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
713
- const authorizedUserId = await this.resolveRealUserId(data.access_token);
688
+ const authorizedUserId = subFromJwt(data.access_token);
714
689
  this._tracker?.track({
715
690
  name: "auth.oauth_callback_success",
716
691
  actorType: "user",
@@ -782,12 +757,11 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
782
757
  throw new McpServerError("Refresh endpoint returned non-JSON response");
783
758
  }
784
759
  oauthTokenRefresh.inc({ result: "success" });
785
- const refreshedUserId = await this.resolveRealUserId(data.access_token);
786
760
  this._tracker?.track({
787
761
  name: "auth.token_refresh_success",
788
762
  actorType: "user",
789
- userId: refreshedUserId,
790
- distinctId: refreshedUserId ?? client.client_id
763
+ userId: subFromJwt(data.access_token),
764
+ distinctId: subFromJwt(data.access_token) ?? client.client_id
791
765
  });
792
766
  logger.info({
793
767
  event: "oauth_token_refresh_ok",
@@ -1231,8 +1205,8 @@ function startHttpServer() {
1231
1205
  common: {
1232
1206
  serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
1233
1207
  // matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
1234
- serviceVersion: "0.3.4",
1235
- buildId: "ce4457b",
1208
+ serviceVersion: "0.3.6",
1209
+ buildId: "3c993f6",
1236
1210
  // mcp tsup TODO: inject git short SHA (like CLI)
1237
1211
  region: process.env.PLAUD_REGION ?? "US",
1238
1212
  env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
@@ -1473,7 +1447,7 @@ function startHttpServer() {
1473
1447
  apiBase,
1474
1448
  staticToken: token
1475
1449
  });
1476
- const mcpServer = new McpServer({ name: "plaud", version: "0.3.4" });
1450
+ const mcpServer = new McpServer({ name: "plaud", version: "0.3.6" });
1477
1451
  registerTools(mcpServer, client, warehouseToolHooks);
1478
1452
  const transport = new StreamableHTTPServerTransport({
1479
1453
  sessionIdGenerator: void 0,
@@ -1481,7 +1455,8 @@ function startHttpServer() {
1481
1455
  enableDnsRebindingProtection: true,
1482
1456
  allowedOrigins: ALLOWED_ORIGINS
1483
1457
  });
1484
- const ctxUserId = req.auth.clientId && req.auth.clientId !== "unknown" ? req.auth.clientId : void 0;
1458
+ const fallbackClientId = req.auth.clientId && req.auth.clientId !== "unknown" ? req.auth.clientId : void 0;
1459
+ const ctxUserId = subFromJwt(token) ?? fallbackClientId;
1485
1460
  const telemetryCtx = {
1486
1461
  userId: ctxUserId,
1487
1462
  requestId: reqId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plaud",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Access your Plaud recordings in Claude",
5
5
  "author": {
6
6
  "name": "Plaud AI"
@@ -22,7 +22,7 @@ metadata:
22
22
  | User wants | Tool | Notes |
23
23
  |---|---|---|
24
24
  | AI summary, TL;DR, action items | `get_note` | Returns Markdown; usually enough — try this before `get_transcript` |
25
- | Verbatim quotes, full dialogue | `get_transcript` | Timestamped; larger |
25
+ | Verbatim quotes, full dialogue | `get_transcript` | Timestamped; paginated — follow `next_cursor` for long recordings |
26
26
  | Audio download link | `get_file` then use `presigned_url` | Link expires in 24h |
27
27
  | Full metadata + availability flags | `get_file` | Check `source_list` / `note_list` populated before claiming content exists |
28
28
 
@@ -47,5 +47,5 @@ Common schemas:
47
47
 
48
48
  ## Anti-patterns
49
49
 
50
- - Do not call `get_transcript` speculatively — it's the largest payload.
50
+ - Do not call `get_transcript` speculatively — it's the largest payload (paged; a long recording needs several `next_cursor` calls).
51
51
  - Do not paraphrase the AI summary unless the user asked; quote it verbatim.