@plaud-ai/mcp 0.3.7 → 0.3.8

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.
@@ -446,6 +446,49 @@ async function loadBlockContent(block) {
446
446
  return "";
447
447
  }
448
448
 
449
+ // ../shared/dist/time.js
450
+ var HAS_TIMEZONE = /(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;
451
+ var HAS_TIME = /\d{2}:\d{2}/;
452
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
453
+ function parseApiTimestamp(value) {
454
+ if (!value)
455
+ return null;
456
+ const trimmed = value.trim();
457
+ if (!trimmed)
458
+ return null;
459
+ const normalised = HAS_TIME.test(trimmed) && !HAS_TIMEZONE.test(trimmed) ? `${trimmed}Z` : trimmed;
460
+ const ms = new Date(normalised).getTime();
461
+ return Number.isNaN(ms) ? null : ms;
462
+ }
463
+ function localDayStart(value) {
464
+ const parts = parseDateOnly(value);
465
+ if (!parts)
466
+ return null;
467
+ const [y, m, d] = parts;
468
+ return new Date(y, m - 1, d, 0, 0, 0, 0).getTime();
469
+ }
470
+ function localDayEnd(value) {
471
+ const parts = parseDateOnly(value);
472
+ if (!parts)
473
+ return null;
474
+ const [y, m, d] = parts;
475
+ return new Date(y, m - 1, d + 1, 0, 0, 0, 0).getTime() - 1;
476
+ }
477
+ function parseDateOnly(value) {
478
+ if (!value)
479
+ return null;
480
+ const match = DATE_ONLY.exec(value.trim());
481
+ if (!match)
482
+ return null;
483
+ const y = Number(match[1]);
484
+ const m = Number(match[2]);
485
+ const d = Number(match[3]);
486
+ const probe = new Date(y, m - 1, d);
487
+ if (probe.getFullYear() !== y || probe.getMonth() !== m - 1 || probe.getDate() !== d)
488
+ return null;
489
+ return [y, m, d];
490
+ }
491
+
449
492
  // ../telemetry/dist/client.js
450
493
  import { PostHog } from "posthog-node";
451
494
 
@@ -510,10 +553,18 @@ async function shutdown() {
510
553
  return;
511
554
  const client = cachedClient;
512
555
  cachedClient = null;
513
- await Promise.race([
514
- client.shutdown(),
515
- new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
516
- ]);
556
+ let timer;
557
+ try {
558
+ await Promise.race([
559
+ client.shutdown(),
560
+ new Promise((resolve) => {
561
+ timer = setTimeout(resolve, SHUTDOWN_TIMEOUT_MS);
562
+ })
563
+ ]);
564
+ } finally {
565
+ if (timer)
566
+ clearTimeout(timer);
567
+ }
517
568
  }
518
569
 
519
570
  // ../telemetry/dist/api.js
@@ -859,6 +910,9 @@ export {
859
910
  PlaudClient,
860
911
  runOAuthCallback,
861
912
  loadBlockContent,
913
+ parseApiTimestamp,
914
+ localDayStart,
915
+ localDayEnd,
862
916
  shutdown,
863
917
  initTelemetry,
864
918
  setUser,
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  capture,
3
3
  classifyError,
4
- loadBlockContent
5
- } from "./chunk-OYI4PXYP.js";
4
+ loadBlockContent,
5
+ localDayEnd,
6
+ localDayStart,
7
+ parseApiTimestamp
8
+ } from "./chunk-5NWKLF3V.js";
6
9
  import {
7
10
  logger
8
11
  } from "./chunk-NPCCDRWQ.js";
@@ -33,12 +36,6 @@ function decodeTranscriptCursor(cursor) {
33
36
  return null;
34
37
  }
35
38
  }
36
- function parseDate(s) {
37
- if (!s) return null;
38
- const d = new Date(s);
39
- if (Number.isNaN(d.getTime())) return null;
40
- return d.getTime();
41
- }
42
39
  function normalizeMcpHost(name) {
43
40
  if (!name) return void 0;
44
41
  const n = name.trim().toLowerCase();
@@ -98,8 +95,8 @@ function registerTools(server, client, hooks) {
98
95
  page: z.number().optional().default(1).describe("Page number (ignored when filters are set)"),
99
96
  page_size: z.number().optional().default(20).describe("Items per page (ignored when filters are set)"),
100
97
  query: z.string().optional().describe("Case-insensitive substring match on recording name"),
101
- date_from: z.string().optional().describe("Start date inclusive, YYYY-MM-DD"),
102
- date_to: z.string().optional().describe("End date inclusive, YYYY-MM-DD")
98
+ date_from: z.string().optional().describe("Start date inclusive, YYYY-MM-DD, interpreted in the server's timezone"),
99
+ date_to: z.string().optional().describe("End date inclusive, YYYY-MM-DD, interpreted in the server's timezone")
103
100
  }
104
101
  },
105
102
  async ({ page, page_size, query, date_from, date_to }) => {
@@ -115,9 +112,8 @@ function registerTools(server, client, hooks) {
115
112
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
116
113
  }
117
114
  const q = query?.toLowerCase();
118
- const from = parseDate(date_from);
119
- const toRaw = parseDate(date_to);
120
- const to = toRaw !== null ? toRaw + 24 * 60 * 60 * 1e3 - 1 : null;
115
+ const from = localDayStart(date_from);
116
+ const to = localDayEnd(date_to);
121
117
  const matches = [];
122
118
  let scanned = 0;
123
119
  let truncated = false;
@@ -128,7 +124,7 @@ function registerTools(server, client, hooks) {
128
124
  for (const item of items) {
129
125
  if (q && !(item.name ?? "").toLowerCase().includes(q)) continue;
130
126
  if (from !== null || to !== null) {
131
- const created = parseDate(item.created_at);
127
+ const created = parseApiTimestamp(item.created_at);
132
128
  if (created === null) continue;
133
129
  if (from !== null && created < from) continue;
134
130
  if (to !== null && created > to) continue;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PlaudClient,
3
3
  capture
4
- } from "./chunk-OYI4PXYP.js";
4
+ } from "./chunk-5NWKLF3V.js";
5
5
  import {
6
6
  httpClientDuration,
7
7
  httpClientRequests
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient
4
- } from "./chunk-3K3M2X74.js";
4
+ } from "./chunk-EY5K2UXG.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-VLZPO2CO.js";
11
+ } from "./chunk-DKOQXKKG.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-OYI4PXYP.js";
24
+ } from "./chunk-5NWKLF3V.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.7"
34
+ version: "0.3.8"
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-3E63G6QX.js");
161
+ const { runInstall } = await import("./install-RTIXREYV.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-2HG4R4LT.js");
194
+ const { startHttpServer } = await import("./server-VJAFEGJ6.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.7",
220
+ appVersion: "0.3.8",
221
221
  transport: "stdio"
222
222
  });
223
223
  } catch {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-3K3M2X74.js";
3
+ } from "./chunk-EY5K2UXG.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-OYI4PXYP.js";
15
+ } from "./chunk-5NWKLF3V.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-VLZPO2CO.js";
4
+ } from "./chunk-DKOQXKKG.js";
5
5
  import {
6
6
  PlaudClient
7
- } from "./chunk-OYI4PXYP.js";
7
+ } from "./chunk-5NWKLF3V.js";
8
8
  import {
9
9
  logger
10
10
  } from "./chunk-NPCCDRWQ.js";
@@ -380,7 +380,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
380
380
  return this._registeredClients.get(id);
381
381
  },
382
382
  registerClient: async (client) => {
383
- const rawId = randomBytes(16).toString("base64url");
383
+ const rawId = this.encodeRawId(client.client_name);
384
384
  const tokenEndpointAuthMethod = client.token_endpoint_auth_method ?? "none";
385
385
  const full = {
386
386
  ...client,
@@ -428,6 +428,34 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
428
428
  const sig = createHmac("sha256", this._clientIdSecret).update(rawId).digest().subarray(0, 16);
429
429
  return `${rawId}.${sig.toString("base64url")}`;
430
430
  }
431
+ // Self-describing client_id: we can't persist the DCR registry across
432
+ // container restarts (compliance — no storage), so we encode the DCR
433
+ // client_name into the signed client_id itself. verifyAndRecover decodes the
434
+ // real name back instead of a placeholder, keeping mcp_host attribution
435
+ // correct across restarts (fixes the "auto-recovered client" bucket). The
436
+ // payload holds the app's self-reported name (not PII, capped) plus a nonce
437
+ // so repeat registrations get distinct ids. base64url contains no ".", so it
438
+ // never collides with the "<rawId>.<sig>" separator.
439
+ encodeRawId(clientName) {
440
+ const name = typeof clientName === "string" && clientName.length > 0 ? clientName.slice(0, 64) : null;
441
+ const payload = JSON.stringify({ v: 1, n: name, r: randomBytes(8).toString("base64url") });
442
+ return Buffer.from(payload, "utf8").toString("base64url");
443
+ }
444
+ // Decode the client_name embedded by encodeRawId. Returns undefined for
445
+ // legacy ids (random rawId issued before this fix) or anything malformed —
446
+ // callers fall back to the placeholder, so recovery still degrades gracefully.
447
+ decodeClientName(clientId) {
448
+ const idx = clientId.lastIndexOf(".");
449
+ const rawId = idx > 0 ? clientId.slice(0, idx) : clientId;
450
+ try {
451
+ const payload = JSON.parse(Buffer.from(rawId, "base64url").toString("utf8"));
452
+ if (payload && payload.v === 1 && typeof payload.n === "string" && payload.n.length > 0) {
453
+ return payload.n;
454
+ }
455
+ } catch {
456
+ }
457
+ return void 0;
458
+ }
431
459
  // Verify a previously-issued client_id. Returns true only for ids whose HMAC
432
460
  // signature matches our secret — i.e. ids this server (or its predecessor with
433
461
  // the same PLAUD_DCR_HMAC_SECRET) issued via registerClient.
@@ -509,6 +537,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
509
537
  if (!this.verifyClientIdSignature(clientId)) {
510
538
  return false;
511
539
  }
540
+ const recoveredName = this.decodeClientName(clientId);
512
541
  this._registeredClients.set(clientId, {
513
542
  client_id: clientId,
514
543
  client_id_issued_at: Math.floor(Date.now() / 1e3),
@@ -516,9 +545,14 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
516
545
  grant_types: ["authorization_code", "refresh_token"],
517
546
  response_types: ["code"],
518
547
  token_endpoint_auth_method: "none",
519
- client_name: "auto-recovered client"
548
+ client_name: recoveredName ?? "auto-recovered client"
549
+ });
550
+ logger.info({
551
+ event: "oauth_client_recovered",
552
+ client_id: clientId,
553
+ redirect_uri: redirectUri ?? null,
554
+ name_recovered: recoveredName != null
520
555
  });
521
- logger.info({ event: "oauth_client_recovered", client_id: clientId, redirect_uri: redirectUri ?? null });
522
556
  return true;
523
557
  }
524
558
  /**
@@ -1205,8 +1239,8 @@ function startHttpServer() {
1205
1239
  common: {
1206
1240
  serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
1207
1241
  // matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
1208
- serviceVersion: "0.3.7",
1209
- buildId: "43da41f",
1242
+ serviceVersion: "0.3.8",
1243
+ buildId: "be0dfaa",
1210
1244
  // mcp tsup TODO: inject git short SHA (like CLI)
1211
1245
  region: process.env.PLAUD_REGION ?? "US",
1212
1246
  env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
@@ -1436,7 +1470,27 @@ function startHttpServer() {
1436
1470
  // register…" once new-registration rate exceeds ~20/hour. Real auth still
1437
1471
  // happens upstream at Plaud; abuse/unbounded-growth should be bounded on
1438
1472
  // the in-memory client store (size/TTL cap) rather than via this per-IP limiter.
1439
- clientRegistrationOptions: { rateLimit: false }
1473
+ clientRegistrationOptions: { rateLimit: false },
1474
+ // Same failure mode on the two handshake endpoints — /register was only
1475
+ // the first one users hit. The SDK also defaults /token to 50 requests per
1476
+ // 15 min and /authorize to 100 per 15 min, both keyed per client IP, and
1477
+ // behind our proxy every request presents the same upstream address, so
1478
+ // the per-IP bucket degenerates into ONE bucket shared by all users
1479
+ // worldwide. A reporter measured this from two continents at once: two
1480
+ // requests 2s apart from Cyprus and Germany saw the same counter (13 and
1481
+ // 11 remaining, reset 671s/672s), and the bucket drained ~13 req/min from
1482
+ // other traffic alone, i.e. it is empty almost continuously.
1483
+ //
1484
+ // /token is the worst place to cap: it is the only handshake endpoint that
1485
+ // repeats after login, because every silent refresh_token renewal goes
1486
+ // through it. Exhausting it does not just block new logins — it breaks
1487
+ // token renewal for users who are already connected.
1488
+ //
1489
+ // Real authentication and abuse control live upstream at Plaud (and at the
1490
+ // edge); this in-process per-IP limiter cannot see past the proxy, so it
1491
+ // can only produce false positives.
1492
+ tokenOptions: { rateLimit: false },
1493
+ authorizationOptions: { rateLimit: false }
1440
1494
  })
1441
1495
  );
1442
1496
  app.post(
@@ -1455,7 +1509,7 @@ function startHttpServer() {
1455
1509
  apiBase,
1456
1510
  staticToken: token
1457
1511
  });
1458
- const mcpServer = new McpServer({ name: "plaud", version: "0.3.7" });
1512
+ const mcpServer = new McpServer({ name: "plaud", version: "0.3.8" });
1459
1513
  registerTools(mcpServer, client, warehouseToolHooks);
1460
1514
  const transport = new StreamableHTTPServerTransport({
1461
1515
  sessionIdGenerator: void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/mcp",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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.7",
3
+ "version": "0.3.8",
4
4
  "description": "Access your Plaud recordings in Claude",
5
5
  "author": {
6
6
  "name": "Plaud AI"