@plaud-ai/mcp 0.3.5 → 0.3.7

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-FDWRBOUI.js";
4
+ } from "./chunk-OYI4PXYP.js";
5
5
  import {
6
6
  httpClientDuration,
7
7
  httpClientRequests
@@ -429,6 +429,23 @@ function runOAuthCallback(opts) {
429
429
  });
430
430
  }
431
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
+
432
449
  // ../telemetry/dist/client.js
433
450
  import { PostHog } from "posthog-node";
434
451
 
@@ -841,6 +858,7 @@ export {
841
858
  clientUserIdFromAccessToken,
842
859
  PlaudClient,
843
860
  runOAuthCallback,
861
+ loadBlockContent,
844
862
  shutdown,
845
863
  initTelemetry,
846
864
  setUser,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  capture,
3
- classifyError
4
- } from "./chunk-FDWRBOUI.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);
@@ -157,9 +174,14 @@ function registerTools(server, client, hooks) {
157
174
  const requestId = emitToolClick("get_file", { file_id });
158
175
  try {
159
176
  const file = await client.getFile(file_id);
177
+ let text = JSON.stringify(file, null, 2);
178
+ if (!file.presigned_url) {
179
+ const synced = !!(file.duration || file.source_list && file.source_list.length);
180
+ text += synced ? "\n\nNote: `presigned_url` (audio download URL) is null for this otherwise-synced recording. This is usually a transient backend signing issue \u2014 retry get_file in a few minutes to obtain the URL." : "\n\nNote: `presigned_url` (audio download URL) is null \u2014 audio may not yet be available for this recording.";
181
+ }
160
182
  recordToolMetric("get_file", "success", Date.now() - start, requestId, { file_id });
161
- logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start });
162
- return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
183
+ logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start, presigned_url_present: !!file.presigned_url });
184
+ return { content: [{ type: "text", text }] };
163
185
  } catch (err) {
164
186
  recordToolMetric("get_file", "error", Date.now() - start, requestId, { file_id }, err);
165
187
  logger.error({ event: "tool_call_error", tool: "get_file", duration_ms: Date.now() - start, error: String(err) });
@@ -198,24 +220,90 @@ function registerTools(server, client, hooks) {
198
220
  server.registerTool(
199
221
  "get_transcript",
200
222
  {
201
- description: "Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
223
+ 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
224
  annotations: {
203
225
  title: "Get recording transcript",
204
226
  readOnlyHint: true,
205
227
  destructiveHint: false,
206
228
  openWorldHint: true
207
229
  },
208
- inputSchema: { file_id: z.string().describe("The file ID to retrieve transcript for") }
230
+ inputSchema: {
231
+ file_id: z.string().describe("The file ID to retrieve transcript for"),
232
+ block: z.enum(TRANSCRIPT_BLOCKS).optional().describe(
233
+ "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`."
234
+ ),
235
+ cursor: z.string().optional().describe("Opaque pagination cursor from a previous call's `next_cursor`. Omit to start from the first utterance."),
236
+ 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.`)
237
+ }
209
238
  },
210
- async ({ file_id }) => {
239
+ async ({ file_id, block, cursor, limit }) => {
211
240
  const start = Date.now();
212
- logger.info({ event: "tool_call", tool: "get_transcript", file_id });
241
+ const blockType = block ?? "transaction";
242
+ logger.info({ event: "tool_call", tool: "get_transcript", file_id, block: blockType });
213
243
  const requestId = emitToolClick("get_transcript", { file_id });
214
244
  try {
215
245
  const file = await client.getFile(file_id);
246
+ const sourceList = file.source_list ?? [];
247
+ if (sourceList.length === 0) {
248
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
249
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, paginated: false });
250
+ return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
251
+ }
252
+ const selected = sourceList.find((s) => s.data_type === blockType);
253
+ if (!selected) {
254
+ const available = sourceList.map((s) => s.data_type).filter(Boolean).join(", ") || "(none)";
255
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
256
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, found: false });
257
+ return {
258
+ content: [{ type: "text", text: `Block "${blockType}" not available for this recording. Available blocks: ${available}.` }]
259
+ };
260
+ }
261
+ const content = await loadBlockContent(selected);
262
+ let segments = null;
263
+ if (content) {
264
+ try {
265
+ const parsed = JSON.parse(content);
266
+ if (Array.isArray(parsed)) segments = parsed;
267
+ } catch {
268
+ segments = null;
269
+ }
270
+ }
271
+ if (!segments) {
272
+ recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
273
+ logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, paginated: false });
274
+ const text = content || `Block "${blockType}" has no content for this recording yet.`;
275
+ return { content: [{ type: "text", text }] };
276
+ }
277
+ let offset = 0;
278
+ if (cursor !== void 0) {
279
+ const decoded = decodeTranscriptCursor(cursor);
280
+ if (decoded === null) {
281
+ recordToolMetric("get_transcript", "error", Date.now() - start, requestId, { file_id }, new Error("invalid_cursor"));
282
+ logger.warn({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, error: "invalid_cursor" });
283
+ return {
284
+ content: [{ type: "text", text: "Invalid cursor. Omit `cursor` to start from the first utterance." }],
285
+ isError: true
286
+ };
287
+ }
288
+ offset = decoded;
289
+ }
290
+ const pageSize = limit ?? TRANSCRIPT_PAGE_SIZE;
291
+ const page = segments.slice(offset, offset + pageSize);
292
+ const nextOffset = offset + page.length;
293
+ const hasMore = nextOffset < segments.length;
294
+ const payload = {
295
+ file_id,
296
+ block: blockType,
297
+ total: segments.length,
298
+ offset,
299
+ limit: pageSize,
300
+ returned: page.length,
301
+ next_cursor: hasMore ? encodeTranscriptCursor(nextOffset) : null,
302
+ segments: page
303
+ };
216
304
  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) }] };
305
+ 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 });
306
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
219
307
  } catch (err) {
220
308
  recordToolMetric("get_transcript", "error", Date.now() - start, requestId, { file_id }, err);
221
309
  logger.error({ event: "tool_call_error", tool: "get_transcript", duration_ms: Date.now() - start, error: String(err) });
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient
4
- } from "./chunk-SEOLTKA5.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-YI4KJEAG.js";
11
+ } from "./chunk-VLZPO2CO.js";
12
12
  import {
13
13
  capture,
14
14
  classifyError,
@@ -21,7 +21,7 @@ import {
21
21
  setMcpHost,
22
22
  setUser,
23
23
  shutdown
24
- } from "./chunk-FDWRBOUI.js";
24
+ } from "./chunk-OYI4PXYP.js";
25
25
  import "./chunk-NPCCDRWQ.js";
26
26
  import "./chunk-RUFCT6DQ.js";
27
27
 
@@ -31,7 +31,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
31
31
  import open from "open";
32
32
  var server = new McpServer({
33
33
  name: "plaud",
34
- version: "0.3.5"
34
+ version: "0.3.7"
35
35
  });
36
36
  var CALLBACK_PORT = 8199;
37
37
  var LOGIN_TIMEOUT_MS = 12e4;
@@ -158,7 +158,7 @@ async function main() {
158
158
  const sub = process.argv[2];
159
159
  const sub2 = process.argv[3];
160
160
  if (sub === "install") {
161
- const { runInstall } = await import("./install-OHOKLBHH.js");
161
+ const { runInstall } = await import("./install-3E63G6QX.js");
162
162
  const args = process.argv.slice(3);
163
163
  const yes = args.some((a) => a === "--yes" || a === "-y");
164
164
  const noLogin = args.some((a) => a === "--no-login");
@@ -191,7 +191,7 @@ async function main() {
191
191
  return;
192
192
  }
193
193
  if (sub === "http") {
194
- const { startHttpServer } = await import("./server-XUKJZCFN.js");
194
+ const { startHttpServer } = await import("./server-2HG4R4LT.js");
195
195
  const { startMetricsServer } = await import("./server-NKCNUA6P.js");
196
196
  startMetricsServer();
197
197
  startHttpServer();
@@ -217,7 +217,7 @@ Usage:
217
217
  try {
218
218
  await initTelemetry({
219
219
  surface: "mcp",
220
- appVersion: "0.3.5",
220
+ appVersion: "0.3.7",
221
221
  transport: "stdio"
222
222
  });
223
223
  } catch {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-SEOLTKA5.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-FDWRBOUI.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-YI4KJEAG.js";
4
+ } from "./chunk-VLZPO2CO.js";
5
5
  import {
6
6
  PlaudClient
7
- } from "./chunk-FDWRBOUI.js";
7
+ } from "./chunk-OYI4PXYP.js";
8
8
  import {
9
9
  logger
10
10
  } from "./chunk-NPCCDRWQ.js";
@@ -1205,8 +1205,8 @@ function startHttpServer() {
1205
1205
  common: {
1206
1206
  serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
1207
1207
  // matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
1208
- serviceVersion: "0.3.5",
1209
- buildId: "38e7de5",
1208
+ serviceVersion: "0.3.7",
1209
+ buildId: "43da41f",
1210
1210
  // mcp tsup TODO: inject git short SHA (like CLI)
1211
1211
  region: process.env.PLAUD_REGION ?? "US",
1212
1212
  env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
@@ -1428,7 +1428,15 @@ function startHttpServer() {
1428
1428
  issuerUrl,
1429
1429
  baseUrl: issuerUrl,
1430
1430
  resourceServerUrl: new URL(mcpResourceUrl),
1431
- resourceName: "Plaud MCP Server"
1431
+ resourceName: "Plaud MCP Server",
1432
+ // Disable the SDK's default DCR rate limit (20/hour, keyed per client IP).
1433
+ // Directory clients like Claude perform Dynamic Client Registration from a
1434
+ // small pool of *shared server-side egress IPs*, so a per-IP cap acts as a
1435
+ // near-global limit and rejects legitimate new users with "Couldn't
1436
+ // register…" once new-registration rate exceeds ~20/hour. Real auth still
1437
+ // happens upstream at Plaud; abuse/unbounded-growth should be bounded on
1438
+ // the in-memory client store (size/TTL cap) rather than via this per-IP limiter.
1439
+ clientRegistrationOptions: { rateLimit: false }
1432
1440
  })
1433
1441
  );
1434
1442
  app.post(
@@ -1447,7 +1455,7 @@ function startHttpServer() {
1447
1455
  apiBase,
1448
1456
  staticToken: token
1449
1457
  });
1450
- const mcpServer = new McpServer({ name: "plaud", version: "0.3.5" });
1458
+ const mcpServer = new McpServer({ name: "plaud", version: "0.3.7" });
1451
1459
  registerTools(mcpServer, client, warehouseToolHooks);
1452
1460
  const transport = new StreamableHTTPServerTransport({
1453
1461
  sessionIdGenerator: void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/mcp",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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.5",
3
+ "version": "0.3.7",
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.