@plaud-ai/mcp 0.3.9 → 0.3.11
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-RUFCT6DQ.js → chunk-DIPROABB.js} +4 -3
- package/dist/{chunk-FSNFNJXH.js → chunk-O6BKY23H.js} +25 -18
- package/dist/chunk-OEZV5MA4.js +20 -0
- package/dist/{chunk-EY5K2UXG.js → chunk-PZSLLSMX.js} +2 -2
- package/dist/{chunk-5NWKLF3V.js → chunk-VUPXBO2J.js} +94 -2
- package/dist/index.js +29 -14
- package/dist/{install-RTIXREYV.js → install-XYADZE7L.js} +15 -5
- package/dist/instrumentation.js +54 -0
- package/dist/{server-SYSE62NO.js → server-KMU2TWA5.js} +477 -91
- package/dist/{server-NKCNUA6P.js → server-P6CNPNJ6.js} +2 -2
- package/package.json +10 -1
- package/dist/chunk-NPCCDRWQ.js +0 -10
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeMcpHost,
|
|
3
3
|
registerTools
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-O6BKY23H.js";
|
|
5
5
|
import {
|
|
6
|
-
PlaudClient
|
|
7
|
-
|
|
6
|
+
PlaudClient,
|
|
7
|
+
classifyError
|
|
8
|
+
} from "./chunk-VUPXBO2J.js";
|
|
8
9
|
import {
|
|
9
10
|
logger
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OEZV5MA4.js";
|
|
11
12
|
import {
|
|
12
13
|
httpRequestDuration,
|
|
13
14
|
httpRequestsInProgress,
|
|
14
15
|
oauthTokenRefresh
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DIPROABB.js";
|
|
16
17
|
|
|
17
18
|
// src/http/server.ts
|
|
18
19
|
import express from "express";
|
|
19
20
|
import { createServer } from "http";
|
|
20
|
-
import { randomUUID as
|
|
21
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
21
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
23
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
24
|
import { createOAuthMetadata, mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
@@ -25,12 +26,13 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
|
|
|
25
26
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
26
27
|
|
|
27
28
|
// src/http/oauth-provider.ts
|
|
28
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes,
|
|
29
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
29
30
|
import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
|
|
30
31
|
import {
|
|
31
32
|
InvalidGrantError,
|
|
32
33
|
InvalidTokenError,
|
|
33
|
-
ServerError as McpServerError
|
|
34
|
+
ServerError as McpServerError,
|
|
35
|
+
TooManyRequestsError
|
|
34
36
|
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
35
37
|
|
|
36
38
|
// src/http/cimd.ts
|
|
@@ -266,6 +268,249 @@ function validateMetadata(parsed, expectedClientIdUrl) {
|
|
|
266
268
|
};
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
// src/http/token-verifier.ts
|
|
272
|
+
import { createHash } from "crypto";
|
|
273
|
+
function decodeJwtClaims(token) {
|
|
274
|
+
const parts = token.split(".");
|
|
275
|
+
if (parts.length !== 3) return null;
|
|
276
|
+
const payload = parts[1];
|
|
277
|
+
if (!payload) return null;
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
280
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
281
|
+
return parsed;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function sha256(value) {
|
|
287
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
288
|
+
}
|
|
289
|
+
var Lru = class {
|
|
290
|
+
constructor(max) {
|
|
291
|
+
this.max = max;
|
|
292
|
+
}
|
|
293
|
+
max;
|
|
294
|
+
map = /* @__PURE__ */ new Map();
|
|
295
|
+
get(key) {
|
|
296
|
+
const value = this.map.get(key);
|
|
297
|
+
if (value === void 0) return void 0;
|
|
298
|
+
this.map.delete(key);
|
|
299
|
+
this.map.set(key, value);
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
set(key, value) {
|
|
303
|
+
if (this.map.has(key)) {
|
|
304
|
+
this.map.delete(key);
|
|
305
|
+
} else if (this.map.size >= this.max) {
|
|
306
|
+
const oldest = this.map.keys().next().value;
|
|
307
|
+
if (oldest !== void 0) this.map.delete(oldest);
|
|
308
|
+
}
|
|
309
|
+
this.map.set(key, value);
|
|
310
|
+
}
|
|
311
|
+
delete(key) {
|
|
312
|
+
this.map.delete(key);
|
|
313
|
+
}
|
|
314
|
+
get size() {
|
|
315
|
+
return this.map.size;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var TokenBucket = class {
|
|
319
|
+
constructor(capacity, refillPerSec, nowMs) {
|
|
320
|
+
this.capacity = capacity;
|
|
321
|
+
this.refillPerSec = refillPerSec;
|
|
322
|
+
this.tokens = capacity;
|
|
323
|
+
this.lastRefillMs = nowMs;
|
|
324
|
+
}
|
|
325
|
+
capacity;
|
|
326
|
+
refillPerSec;
|
|
327
|
+
tokens;
|
|
328
|
+
lastRefillMs;
|
|
329
|
+
take(nowMs) {
|
|
330
|
+
const elapsedSec = (nowMs - this.lastRefillMs) / 1e3;
|
|
331
|
+
if (elapsedSec > 0) {
|
|
332
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec);
|
|
333
|
+
this.lastRefillMs = nowMs;
|
|
334
|
+
}
|
|
335
|
+
if (this.tokens >= 1) {
|
|
336
|
+
this.tokens -= 1;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var DEFAULTS = {
|
|
343
|
+
cacheMaxEntries: 5e3,
|
|
344
|
+
positiveTtlMs: 6e4,
|
|
345
|
+
negativeTtlMs: 3e4,
|
|
346
|
+
strangerBurst: 40,
|
|
347
|
+
strangerRatePerSec: 20,
|
|
348
|
+
knownBurst: 10,
|
|
349
|
+
knownRatePerSec: 5,
|
|
350
|
+
knownSubMaxEntries: 2e4
|
|
351
|
+
};
|
|
352
|
+
var TokenVerifier = class {
|
|
353
|
+
cache;
|
|
354
|
+
subBuckets;
|
|
355
|
+
strangerBucket;
|
|
356
|
+
opts;
|
|
357
|
+
constructor(options) {
|
|
358
|
+
const now = options.now ?? (() => Date.now());
|
|
359
|
+
this.opts = {
|
|
360
|
+
verifyUpstream: options.verifyUpstream,
|
|
361
|
+
cacheEnabled: options.cacheEnabled ?? true,
|
|
362
|
+
cacheMaxEntries: options.cacheMaxEntries ?? DEFAULTS.cacheMaxEntries,
|
|
363
|
+
positiveTtlMs: options.positiveTtlMs ?? DEFAULTS.positiveTtlMs,
|
|
364
|
+
negativeTtlMs: options.negativeTtlMs ?? DEFAULTS.negativeTtlMs,
|
|
365
|
+
strangerBurst: options.strangerBurst ?? DEFAULTS.strangerBurst,
|
|
366
|
+
strangerRatePerSec: options.strangerRatePerSec ?? DEFAULTS.strangerRatePerSec,
|
|
367
|
+
knownBurst: options.knownBurst ?? DEFAULTS.knownBurst,
|
|
368
|
+
knownRatePerSec: options.knownRatePerSec ?? DEFAULTS.knownRatePerSec,
|
|
369
|
+
knownSubMaxEntries: options.knownSubMaxEntries ?? DEFAULTS.knownSubMaxEntries,
|
|
370
|
+
now
|
|
371
|
+
};
|
|
372
|
+
this.cache = new Lru(this.opts.cacheMaxEntries);
|
|
373
|
+
this.subBuckets = new Lru(this.opts.knownSubMaxEntries);
|
|
374
|
+
this.strangerBucket = new TokenBucket(
|
|
375
|
+
this.opts.strangerBurst,
|
|
376
|
+
this.opts.strangerRatePerSec,
|
|
377
|
+
now()
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Seeds the cache with a token that was just minted upstream (OAuth code
|
|
382
|
+
* exchange or refresh).
|
|
383
|
+
*
|
|
384
|
+
* This is what keeps normal users out of the rate-limit tier entirely: their
|
|
385
|
+
* first request after logging in or rotating a token is already a cache hit,
|
|
386
|
+
* so it never draws from the budget that unrecognised tokens consume.
|
|
387
|
+
*
|
|
388
|
+
* clientId is taken from the JWT `sub` rather than the upstream user.id: we
|
|
389
|
+
* have not called upstream at this point and do not need to, since the token
|
|
390
|
+
* came straight from the token endpoint. Downstream only uses clientId for
|
|
391
|
+
* logging, and server.ts derives ctxUserId from `sub` first anyway.
|
|
392
|
+
*/
|
|
393
|
+
prewarm(token) {
|
|
394
|
+
if (!this.opts.cacheEnabled) return;
|
|
395
|
+
const claims = decodeJwtClaims(token);
|
|
396
|
+
if (!claims) return;
|
|
397
|
+
const sub = typeof claims.sub === "string" ? claims.sub : void 0;
|
|
398
|
+
const exp = typeof claims.exp === "number" ? claims.exp : void 0;
|
|
399
|
+
if (!sub) return;
|
|
400
|
+
const nowMs = this.opts.now();
|
|
401
|
+
const expiresAt = exp ?? Math.floor(nowMs / 1e3) + 3600;
|
|
402
|
+
this.putPositive(sha256(token), sub, expiresAt, nowMs);
|
|
403
|
+
this.noteKnownSub(sub, nowMs);
|
|
404
|
+
logger.info({ event: "token_prewarmed", cache_size: this.cache.size });
|
|
405
|
+
}
|
|
406
|
+
async verify(token) {
|
|
407
|
+
const nowMs = this.opts.now();
|
|
408
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
409
|
+
const claims = decodeJwtClaims(token);
|
|
410
|
+
if (!claims) {
|
|
411
|
+
logger.warn({ event: "token_rejected_local", reason: "malformed" });
|
|
412
|
+
return { ok: false, kind: "malformed" };
|
|
413
|
+
}
|
|
414
|
+
const sub = typeof claims.sub === "string" && claims.sub ? claims.sub : void 0;
|
|
415
|
+
const exp = typeof claims.exp === "number" ? claims.exp : void 0;
|
|
416
|
+
if (exp !== void 0 && exp <= nowSec) {
|
|
417
|
+
logger.warn({ event: "token_rejected_local", reason: "expired" });
|
|
418
|
+
return { ok: false, kind: "expired" };
|
|
419
|
+
}
|
|
420
|
+
const key = sha256(token);
|
|
421
|
+
if (this.opts.cacheEnabled) {
|
|
422
|
+
const hit = this.cache.get(key);
|
|
423
|
+
if (hit && hit.cacheExpiresAtMs > nowMs) {
|
|
424
|
+
return hit.valid ? { ok: true, clientId: hit.clientId, expiresAt: hit.expiresAt, source: "cache" } : { ok: false, kind: "invalid" };
|
|
425
|
+
}
|
|
426
|
+
if (hit) this.cache.delete(key);
|
|
427
|
+
}
|
|
428
|
+
const looksKnown = sub !== void 0 && sub.startsWith("client_user_");
|
|
429
|
+
const bucket = sub !== void 0 && looksKnown ? this.subBuckets.get(sub) : void 0;
|
|
430
|
+
const allowed = bucket ? bucket.take(nowMs) : this.strangerBucket.take(nowMs);
|
|
431
|
+
if (!allowed) {
|
|
432
|
+
logger.warn({
|
|
433
|
+
event: "token_verify_rate_limited",
|
|
434
|
+
scope: bucket ? "known_sub" : "stranger"
|
|
435
|
+
});
|
|
436
|
+
return { ok: false, kind: "rate_limited" };
|
|
437
|
+
}
|
|
438
|
+
const verdict = await this.opts.verifyUpstream(token);
|
|
439
|
+
if (verdict.kind === "valid") {
|
|
440
|
+
const expiresAt = exp ?? nowSec + 3600;
|
|
441
|
+
if (this.opts.cacheEnabled) this.putPositive(key, verdict.clientId, expiresAt, nowMs);
|
|
442
|
+
if (sub !== void 0) this.noteKnownSub(sub, nowMs);
|
|
443
|
+
logger.info({ event: "token_verified", client_id: verdict.clientId, expires_at: expiresAt });
|
|
444
|
+
return { ok: true, clientId: verdict.clientId, expiresAt, source: "upstream" };
|
|
445
|
+
}
|
|
446
|
+
if (verdict.kind === "invalid") {
|
|
447
|
+
if (this.opts.cacheEnabled) {
|
|
448
|
+
this.cache.set(key, {
|
|
449
|
+
valid: false,
|
|
450
|
+
clientId: "",
|
|
451
|
+
expiresAt: 0,
|
|
452
|
+
cacheExpiresAtMs: nowMs + this.opts.negativeTtlMs
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
logger.warn({ event: "token_verify_failed", reason: "invalid" });
|
|
456
|
+
return { ok: false, kind: "invalid" };
|
|
457
|
+
}
|
|
458
|
+
if (verdict.kind === "rate_limited") {
|
|
459
|
+
logger.warn({ event: "token_verify_upstream_rate_limited" });
|
|
460
|
+
return { ok: false, kind: "rate_limited" };
|
|
461
|
+
}
|
|
462
|
+
logger.error({ event: "token_verify_unavailable", reason: verdict.reason });
|
|
463
|
+
return { ok: false, kind: "unavailable" };
|
|
464
|
+
}
|
|
465
|
+
/** For /health and metrics — counts only, no tokens or user identifiers. */
|
|
466
|
+
stats() {
|
|
467
|
+
return {
|
|
468
|
+
cacheSize: this.cache.size,
|
|
469
|
+
knownSubs: this.subBuckets.size,
|
|
470
|
+
cacheEnabled: this.opts.cacheEnabled
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
putPositive(key, clientId, expiresAt, nowMs) {
|
|
474
|
+
const remainingMs = expiresAt * 1e3 - nowMs;
|
|
475
|
+
const ttlMs = Math.min(this.opts.positiveTtlMs, Math.max(0, remainingMs));
|
|
476
|
+
if (ttlMs <= 0) return;
|
|
477
|
+
this.cache.set(key, { valid: true, clientId, expiresAt, cacheExpiresAtMs: nowMs + ttlMs });
|
|
478
|
+
}
|
|
479
|
+
noteKnownSub(sub, nowMs) {
|
|
480
|
+
if (this.subBuckets.get(sub) === void 0) {
|
|
481
|
+
this.subBuckets.set(
|
|
482
|
+
sub,
|
|
483
|
+
new TokenBucket(this.opts.knownBurst, this.opts.knownRatePerSec, nowMs)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
function tokenVerifierOptionsFromEnv() {
|
|
489
|
+
const num = (name, fallback) => {
|
|
490
|
+
const raw = process.env[name];
|
|
491
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
492
|
+
const parsed = Number(raw);
|
|
493
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
494
|
+
logger.warn({ event: "token_verifier_bad_env", name, value: raw, using: fallback });
|
|
495
|
+
return fallback;
|
|
496
|
+
}
|
|
497
|
+
return parsed;
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
cacheEnabled: !["0", "false", "no", "off"].includes(
|
|
501
|
+
(process.env["PLAUD_TOKEN_CACHE_ENABLED"] ?? "").toLowerCase()
|
|
502
|
+
),
|
|
503
|
+
cacheMaxEntries: num("PLAUD_TOKEN_CACHE_MAX", DEFAULTS.cacheMaxEntries),
|
|
504
|
+
positiveTtlMs: num("PLAUD_TOKEN_CACHE_TTL_MS", DEFAULTS.positiveTtlMs),
|
|
505
|
+
negativeTtlMs: num("PLAUD_TOKEN_NEGATIVE_TTL_MS", DEFAULTS.negativeTtlMs),
|
|
506
|
+
strangerBurst: num("PLAUD_TOKEN_VERIFY_BURST", DEFAULTS.strangerBurst),
|
|
507
|
+
strangerRatePerSec: num("PLAUD_TOKEN_VERIFY_RATE", DEFAULTS.strangerRatePerSec),
|
|
508
|
+
knownBurst: num("PLAUD_TOKEN_KNOWN_BURST", DEFAULTS.knownBurst),
|
|
509
|
+
knownRatePerSec: num("PLAUD_TOKEN_KNOWN_RATE", DEFAULTS.knownRatePerSec),
|
|
510
|
+
knownSubMaxEntries: num("PLAUD_TOKEN_KNOWN_SUB_MAX", DEFAULTS.knownSubMaxEntries)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
269
514
|
// src/http/oauth-provider.ts
|
|
270
515
|
function subFromJwt(token) {
|
|
271
516
|
if (!token) return void 0;
|
|
@@ -276,6 +521,10 @@ function subFromJwt(token) {
|
|
|
276
521
|
return void 0;
|
|
277
522
|
}
|
|
278
523
|
}
|
|
524
|
+
function isUpstreamRateLimited(err) {
|
|
525
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
526
|
+
return /\bAPI error: 429\b/.test(msg);
|
|
527
|
+
}
|
|
279
528
|
var AUTO_RECOVERED_CLIENT_NAME = "auto-recovered client";
|
|
280
529
|
var REDIRECT_HOST_CLIENT_NAMES = {
|
|
281
530
|
"chatgpt.com": "chatgpt",
|
|
@@ -301,6 +550,8 @@ var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-par
|
|
|
301
550
|
var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
|
|
302
551
|
var STATELESS_CODE_PREFIX = "pc1_";
|
|
303
552
|
var AUTHORIZATION_CODE_TTL_MS = 10 * 60 * 1e3;
|
|
553
|
+
var STATELESS_STATE_PREFIX = "st1_";
|
|
554
|
+
var AUTHORIZATION_STATE_TTL_MS = 30 * 60 * 1e3;
|
|
304
555
|
function urlParts(value) {
|
|
305
556
|
if (!value || !URL.canParse(value)) {
|
|
306
557
|
return { host: null, path: null };
|
|
@@ -318,57 +569,74 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
318
569
|
_debugOAuthLogs;
|
|
319
570
|
_cimdLoader;
|
|
320
571
|
_tracker;
|
|
572
|
+
/** Local pre-checks + cache + tiered rate limit + upstream call, all in one
|
|
573
|
+
* place. See token-verifier.ts. */
|
|
574
|
+
_verifier;
|
|
321
575
|
_registeredClients = /* @__PURE__ */ new Map();
|
|
322
576
|
// HMAC secret used to sign DCR-issued client_ids so we can recover them
|
|
323
577
|
// across container restarts without persistent storage. See verifyAndRecover.
|
|
324
578
|
_clientIdSecret;
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
579
|
+
// NOTE: there is deliberately no in-process store for pending authorizations.
|
|
580
|
+
// This class used to keep `_pendingStates: Map<state, {clientRedirectUri, …}>`,
|
|
581
|
+
// which meant /authorize and /auth/callback had to be served by the same
|
|
582
|
+
// process — the callback looked the state up and hard-failed with 400 if it
|
|
583
|
+
// wasn't there. That single map is what pinned the deployment to one replica
|
|
584
|
+
// (and it was never swept, so abandoned authorizations accumulated for the
|
|
585
|
+
// lifetime of the pod). The state parameter is now a sealed, self-contained
|
|
586
|
+
// envelope instead; see encodeAuthorizationState.
|
|
587
|
+
//
|
|
588
|
+
// Everything else here was already replica-safe: client_ids are HMAC-signed
|
|
589
|
+
// and recoverable (verifyAndRecover), and authorization codes are sealed the
|
|
590
|
+
// same way. This was the last piece holding it back.
|
|
328
591
|
constructor(options) {
|
|
329
592
|
const authUrl = options.authUrl ?? "https://web.plaud.ai/platform/oauth";
|
|
330
593
|
const tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
331
594
|
const refreshUrl = options.refreshUrl ?? DEFAULT_REFRESH_URL;
|
|
332
595
|
const apiBase = options.apiBase ?? "https://platform.plaud.ai/developer/api";
|
|
596
|
+
const holder = {};
|
|
597
|
+
const verifyUpstream = async (token) => {
|
|
598
|
+
const client = new PlaudClient({
|
|
599
|
+
clientId: options.clientId,
|
|
600
|
+
clientSecret: "",
|
|
601
|
+
redirectUri: "",
|
|
602
|
+
apiBase,
|
|
603
|
+
staticToken: token
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
const user = await client.getCurrentUser();
|
|
607
|
+
return { kind: "valid", clientId: String(user.id ?? "unknown") };
|
|
608
|
+
} catch (err) {
|
|
609
|
+
const type = classifyError(err);
|
|
610
|
+
if (type === "auth") return { kind: "invalid" };
|
|
611
|
+
if (isUpstreamRateLimited(err)) return { kind: "rate_limited" };
|
|
612
|
+
return { kind: "unavailable", reason: type };
|
|
613
|
+
}
|
|
614
|
+
};
|
|
333
615
|
super({
|
|
334
616
|
endpoints: {
|
|
335
617
|
authorizationUrl: authUrl,
|
|
336
618
|
tokenUrl
|
|
337
619
|
},
|
|
338
620
|
verifyAccessToken: async (token) => {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
);
|
|
353
|
-
expiresAt = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + 3600;
|
|
354
|
-
} catch {
|
|
355
|
-
expiresAt = Math.floor(Date.now() / 1e3) + 3600;
|
|
356
|
-
}
|
|
357
|
-
const authInfo = {
|
|
358
|
-
token,
|
|
359
|
-
clientId: String(user.id ?? "unknown"),
|
|
360
|
-
scopes: [],
|
|
361
|
-
expiresAt
|
|
362
|
-
};
|
|
363
|
-
logger.info({ event: "token_verified", client_id: authInfo.clientId, expires_at: expiresAt });
|
|
364
|
-
return authInfo;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
logger.warn({ event: "token_verify_failed", error: String(err) });
|
|
367
|
-
throw new InvalidTokenError("Invalid or expired token");
|
|
621
|
+
const verifier = holder.verifier;
|
|
622
|
+
if (!verifier) throw new McpServerError("Token verifier not initialised");
|
|
623
|
+
const outcome = await verifier.verify(token);
|
|
624
|
+
if (outcome.ok) {
|
|
625
|
+
return { token, clientId: outcome.clientId, scopes: [], expiresAt: outcome.expiresAt };
|
|
626
|
+
}
|
|
627
|
+
switch (outcome.kind) {
|
|
628
|
+
case "unavailable":
|
|
629
|
+
throw new McpServerError("Upstream token verification temporarily unavailable");
|
|
630
|
+
case "rate_limited":
|
|
631
|
+
throw new TooManyRequestsError("Token verification rate limited");
|
|
632
|
+
default:
|
|
633
|
+
throw new InvalidTokenError("Invalid or expired token");
|
|
368
634
|
}
|
|
369
635
|
},
|
|
370
636
|
getClient: async (id) => this._registeredClients.get(id)
|
|
371
637
|
});
|
|
638
|
+
holder.verifier = new TokenVerifier({ ...tokenVerifierOptionsFromEnv(), verifyUpstream });
|
|
639
|
+
this._verifier = holder.verifier;
|
|
372
640
|
this._plaudClientId = options.clientId;
|
|
373
641
|
this._plaudClientSecret = options.clientSecret ?? "";
|
|
374
642
|
this._plaudTokenUrl = tokenUrl;
|
|
@@ -390,6 +658,10 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
390
658
|
}
|
|
391
659
|
this.skipLocalPkceValidation = true;
|
|
392
660
|
}
|
|
661
|
+
/** For /health and metrics — counts only, no tokens or user identifiers. */
|
|
662
|
+
get verifierStats() {
|
|
663
|
+
return this._verifier.stats();
|
|
664
|
+
}
|
|
393
665
|
// Override clientsStore. registerClient signs the issued client_id with HMAC
|
|
394
666
|
// so verifyAndRecover can later resurrect it across container restarts.
|
|
395
667
|
get clientsStore() {
|
|
@@ -495,49 +767,116 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
495
767
|
if (actual.length !== expected.length) return false;
|
|
496
768
|
return timingSafeEqual(actual, expected);
|
|
497
769
|
}
|
|
498
|
-
|
|
499
|
-
|
|
770
|
+
// Key is derived per purpose so a value sealed for one hop can never be
|
|
771
|
+
// unsealed as the other — an authorization code presented as a state (or vice
|
|
772
|
+
// versa) fails the GCM tag check rather than decoding into the wrong shape.
|
|
773
|
+
envelopeKey(purpose) {
|
|
774
|
+
return createHmac("sha256", this._clientIdSecret).update(purpose).digest();
|
|
500
775
|
}
|
|
501
|
-
|
|
776
|
+
// AES-256-GCM envelope: prefix + base64url(iv[12] | tag[16] | ciphertext).
|
|
777
|
+
// Self-contained, so any pod can open what any other pod sealed — that is what
|
|
778
|
+
// lets us run more than one replica without a shared store.
|
|
779
|
+
seal(purpose, prefix, payload, ttlMs) {
|
|
502
780
|
const iv = randomBytes(12);
|
|
503
|
-
const cipher = createCipheriv("aes-256-gcm", this.
|
|
504
|
-
const plaintext = Buffer.from(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
expiresAt: Date.now() + AUTHORIZATION_CODE_TTL_MS
|
|
509
|
-
}), "utf8");
|
|
781
|
+
const cipher = createCipheriv("aes-256-gcm", this.envelopeKey(purpose), iv);
|
|
782
|
+
const plaintext = Buffer.from(
|
|
783
|
+
JSON.stringify({ ...payload, expiresAt: Date.now() + ttlMs }),
|
|
784
|
+
"utf8"
|
|
785
|
+
);
|
|
510
786
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
511
787
|
const tag = cipher.getAuthTag();
|
|
512
|
-
return `${
|
|
788
|
+
return `${prefix}${Buffer.concat([iv, tag, ciphertext]).toString("base64url")}`;
|
|
513
789
|
}
|
|
514
|
-
|
|
515
|
-
|
|
790
|
+
// Returns null for anything that isn't ours, has been tampered with, or has
|
|
791
|
+
// expired. Callers still validate the payload's own shape.
|
|
792
|
+
unseal(purpose, prefix, value) {
|
|
793
|
+
if (typeof value !== "string" || !value.startsWith(prefix)) return null;
|
|
516
794
|
try {
|
|
517
|
-
const
|
|
518
|
-
const data = Buffer.from(encoded, "base64url");
|
|
795
|
+
const data = Buffer.from(value.slice(prefix.length), "base64url");
|
|
519
796
|
if (data.length <= 28) return null;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
797
|
+
const decipher = createDecipheriv("aes-256-gcm", this.envelopeKey(purpose), data.subarray(0, 12));
|
|
798
|
+
decipher.setAuthTag(data.subarray(12, 28));
|
|
799
|
+
const plaintext = Buffer.concat([
|
|
800
|
+
decipher.update(data.subarray(28)),
|
|
801
|
+
decipher.final()
|
|
802
|
+
]).toString("utf8");
|
|
526
803
|
const payload = JSON.parse(plaintext);
|
|
527
|
-
if (typeof payload
|
|
804
|
+
if (typeof payload["expiresAt"] !== "number" || Date.now() > payload["expiresAt"]) {
|
|
528
805
|
return null;
|
|
529
806
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
807
|
+
return payload;
|
|
808
|
+
} catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
encodeAuthorizationCode(payload) {
|
|
813
|
+
return this.seal(
|
|
814
|
+
"plaud-mcp-authorization-code",
|
|
815
|
+
STATELESS_CODE_PREFIX,
|
|
816
|
+
{
|
|
534
817
|
upstreamCode: payload.upstreamCode,
|
|
535
818
|
upstreamState: payload.upstreamState,
|
|
536
819
|
resource: payload.resource
|
|
537
|
-
}
|
|
538
|
-
|
|
820
|
+
},
|
|
821
|
+
AUTHORIZATION_CODE_TTL_MS
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
decodeAuthorizationCode(code) {
|
|
825
|
+
const payload = this.unseal(
|
|
826
|
+
"plaud-mcp-authorization-code",
|
|
827
|
+
STATELESS_CODE_PREFIX,
|
|
828
|
+
code
|
|
829
|
+
);
|
|
830
|
+
if (!payload) return null;
|
|
831
|
+
if (typeof payload.upstreamCode !== "string" || typeof payload.upstreamState !== "string" || payload.resource !== void 0 && typeof payload.resource !== "string") {
|
|
539
832
|
return null;
|
|
540
833
|
}
|
|
834
|
+
return {
|
|
835
|
+
upstreamCode: payload.upstreamCode,
|
|
836
|
+
upstreamState: payload.upstreamState,
|
|
837
|
+
resource: payload.resource
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
// The authorize→callback hop. Everything the callback needs travels inside the
|
|
841
|
+
// state parameter itself, so the pod that handles the callback need not be the
|
|
842
|
+
// pod that started the flow. This is what replaced the in-process
|
|
843
|
+
// _pendingStates map — see the class docstring for why that map capped us at
|
|
844
|
+
// one replica.
|
|
845
|
+
encodeAuthorizationState(payload) {
|
|
846
|
+
return this.seal(
|
|
847
|
+
"plaud-mcp-authorization-state",
|
|
848
|
+
STATELESS_STATE_PREFIX,
|
|
849
|
+
{
|
|
850
|
+
clientRedirectUri: payload.clientRedirectUri,
|
|
851
|
+
originalState: payload.originalState,
|
|
852
|
+
resource: payload.resource
|
|
853
|
+
},
|
|
854
|
+
AUTHORIZATION_STATE_TTL_MS
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
// Stable short handle for correlating authorize and callback log lines.
|
|
858
|
+
// Not a secret and not reversible — purely a join key for reading logs.
|
|
859
|
+
stateHandle(state) {
|
|
860
|
+
if (typeof state !== "string") return "non-string";
|
|
861
|
+
return createHash2("sha256").update(state).digest("base64url").slice(0, 12);
|
|
862
|
+
}
|
|
863
|
+
decodeAuthorizationState(state) {
|
|
864
|
+
const payload = this.unseal(
|
|
865
|
+
"plaud-mcp-authorization-state",
|
|
866
|
+
STATELESS_STATE_PREFIX,
|
|
867
|
+
state
|
|
868
|
+
);
|
|
869
|
+
if (!payload) return null;
|
|
870
|
+
if (typeof payload.clientRedirectUri !== "string" || !URL.canParse(payload.clientRedirectUri)) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
if (payload.originalState !== void 0 && typeof payload.originalState !== "string") return null;
|
|
874
|
+
if (payload.resource !== void 0 && typeof payload.resource !== "string") return null;
|
|
875
|
+
return {
|
|
876
|
+
clientRedirectUri: payload.clientRedirectUri,
|
|
877
|
+
originalState: payload.originalState,
|
|
878
|
+
resource: payload.resource
|
|
879
|
+
};
|
|
541
880
|
}
|
|
542
881
|
// Recover a client_id that the registry doesn't know about. Directory clients
|
|
543
882
|
// (OpenAI Apps, Claude directory) cache the client_id they got from /register;
|
|
@@ -601,8 +940,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
601
940
|
* Store the client's original redirect_uri keyed by state so we can forward after Plaud calls back.
|
|
602
941
|
*/
|
|
603
942
|
async authorize(_client, params, res) {
|
|
604
|
-
const internalState =
|
|
605
|
-
this._pendingStates.set(internalState, {
|
|
943
|
+
const internalState = this.encodeAuthorizationState({
|
|
606
944
|
clientRedirectUri: params.redirectUri,
|
|
607
945
|
originalState: params.state,
|
|
608
946
|
resource: params.resource?.href
|
|
@@ -610,7 +948,10 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
610
948
|
const redirectParts = urlParts(params.redirectUri);
|
|
611
949
|
logger.info({
|
|
612
950
|
event: "oauth_authorize_start",
|
|
613
|
-
|
|
951
|
+
// Short digest, not the sealed blob — it is 300+ chars and would bloat
|
|
952
|
+
// every line. Same value appears on the matching callback, so the two
|
|
953
|
+
// still correlate.
|
|
954
|
+
state_handle: this.stateHandle(internalState),
|
|
614
955
|
redirect_uri: params.redirectUri,
|
|
615
956
|
...this._debugOAuthLogs ? {
|
|
616
957
|
redirect_uri_host: redirectParts.host,
|
|
@@ -618,7 +959,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
618
959
|
has_original_state: !!params.state,
|
|
619
960
|
has_resource: !!params.resource,
|
|
620
961
|
resource: params.resource?.href ?? null,
|
|
621
|
-
|
|
962
|
+
state_len: internalState.length
|
|
622
963
|
} : {}
|
|
623
964
|
});
|
|
624
965
|
const targetUrl = new URL(this._endpoints.authorizationUrl);
|
|
@@ -644,17 +985,25 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
644
985
|
* Called when Plaud redirects to our /auth/callback (CALLBACK_PATH in
|
|
645
986
|
* http/server.ts). NOT /oauth/callback — this comment said so until 2026-08-19
|
|
646
987
|
* and a downstream client registration was configured from it by mistake.
|
|
647
|
-
*
|
|
988
|
+
* Opens the sealed state to recover the client's redirect_uri and forwards the
|
|
989
|
+
* code+state to it. No lookup — the state is the storage, which is what allows
|
|
990
|
+
* this to be served by a pod that never saw the /authorize request.
|
|
648
991
|
*/
|
|
649
992
|
handleCallback(code, state, res) {
|
|
650
|
-
const pending = this.
|
|
993
|
+
const pending = this.decodeAuthorizationState(state);
|
|
651
994
|
if (!pending) {
|
|
652
|
-
logger.warn({
|
|
995
|
+
logger.warn({
|
|
996
|
+
event: "oauth_callback_unknown_state",
|
|
997
|
+
state_handle: this.stateHandle(state),
|
|
998
|
+
// typeof-guarded: this is the one log line reached by a request whose
|
|
999
|
+
// parameter shape we do not control.
|
|
1000
|
+
state_len: typeof state === "string" ? state.length : null,
|
|
1001
|
+
sealed_shape: typeof state === "string" && state.startsWith(STATELESS_STATE_PREFIX)
|
|
1002
|
+
});
|
|
653
1003
|
res.status(400).send("Unknown state \u2014 authorization request not found");
|
|
654
1004
|
return;
|
|
655
1005
|
}
|
|
656
|
-
this.
|
|
657
|
-
logger.info({ event: "oauth_callback_received", internal_state: state });
|
|
1006
|
+
logger.info({ event: "oauth_callback_received", state_handle: this.stateHandle(state) });
|
|
658
1007
|
const pendingCode = {
|
|
659
1008
|
upstreamCode: code,
|
|
660
1009
|
upstreamState: state,
|
|
@@ -667,10 +1016,12 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
667
1016
|
target.searchParams.set("state", pending.originalState);
|
|
668
1017
|
}
|
|
669
1018
|
const redirectParts = urlParts(pending.clientRedirectUri);
|
|
1019
|
+
const isCustomScheme = !/^https?:$/.test(new URL(target.toString()).protocol);
|
|
670
1020
|
logger.info({
|
|
671
1021
|
event: "oauth_callback_redirect",
|
|
672
|
-
|
|
1022
|
+
state_handle: this.stateHandle(state),
|
|
673
1023
|
redirect_uri: pending.clientRedirectUri,
|
|
1024
|
+
custom_scheme: isCustomScheme,
|
|
674
1025
|
has_original_state: !!pending.originalState,
|
|
675
1026
|
original_state_len: pending.originalState?.length ?? 0,
|
|
676
1027
|
upstream_code_len: code.length,
|
|
@@ -685,7 +1036,37 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
685
1036
|
redirect_has_state: target.searchParams.has("state")
|
|
686
1037
|
} : {}
|
|
687
1038
|
});
|
|
688
|
-
|
|
1039
|
+
if (!isCustomScheme) {
|
|
1040
|
+
res.redirect(target.toString());
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
res.status(200).type("html").send(this.appHandoffPage(target.toString()));
|
|
1044
|
+
}
|
|
1045
|
+
// Landing page for native-app callbacks. The URL goes into a data attribute
|
|
1046
|
+
// rather than inline JS so nothing from `clientRedirectUri` can be parsed as
|
|
1047
|
+
// script; it is only ever assigned to location.href.
|
|
1048
|
+
appHandoffPage(redirectUrl) {
|
|
1049
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1050
|
+
const href = esc(redirectUrl);
|
|
1051
|
+
return `<!doctype html>
|
|
1052
|
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
1053
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1054
|
+
<title>\u6388\u6743\u6210\u529F \xB7 Plaud</title></head>
|
|
1055
|
+
<body style="font-family:system-ui,-apple-system,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#fafafa;color:#111">
|
|
1056
|
+
<main id="p" data-href="${href}" style="text-align:center;padding:2rem;max-width:28rem">
|
|
1057
|
+
<div style="font-size:2.5rem;line-height:1">\u2713</div>
|
|
1058
|
+
<h1 style="font-size:1.25rem;margin:.75rem 0 .5rem">\u6388\u6743\u6210\u529F</h1>
|
|
1059
|
+
<p style="color:#666;margin:0 0 1.5rem">\u6B63\u5728\u8FD4\u56DE\u5E94\u7528\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002</p>
|
|
1060
|
+
<p style="font-size:.875rem;color:#888;margin:0">
|
|
1061
|
+
\u6CA1\u6709\u81EA\u52A8\u8DF3\u8F6C\uFF1F<a id="m" href="#" style="color:#0066cc">\u70B9\u6B64\u624B\u52A8\u6253\u5F00</a>
|
|
1062
|
+
</p>
|
|
1063
|
+
</main>
|
|
1064
|
+
<script>
|
|
1065
|
+
var u = document.getElementById("p").dataset.href;
|
|
1066
|
+
document.getElementById("m").href = u;
|
|
1067
|
+
location.href = u;
|
|
1068
|
+
</script>
|
|
1069
|
+
</body></html>`;
|
|
689
1070
|
}
|
|
690
1071
|
// Override to use PKCE public-client flow — no Basic auth, client_id sent in body.
|
|
691
1072
|
async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, _redirectUri, resource) {
|
|
@@ -762,6 +1143,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
762
1143
|
throw new McpServerError("Token endpoint returned non-JSON response");
|
|
763
1144
|
}
|
|
764
1145
|
logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
|
|
1146
|
+
this._verifier.prewarm(data.access_token);
|
|
765
1147
|
const authorizedUserId = subFromJwt(data.access_token);
|
|
766
1148
|
this._tracker?.track({
|
|
767
1149
|
name: "auth.oauth_callback_success",
|
|
@@ -834,6 +1216,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
834
1216
|
throw new McpServerError("Refresh endpoint returned non-JSON response");
|
|
835
1217
|
}
|
|
836
1218
|
oauthTokenRefresh.inc({ result: "success" });
|
|
1219
|
+
this._verifier.prewarm(data.access_token);
|
|
837
1220
|
this._tracker?.track({
|
|
838
1221
|
name: "auth.token_refresh_success",
|
|
839
1222
|
actorType: "user",
|
|
@@ -856,7 +1239,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
856
1239
|
};
|
|
857
1240
|
|
|
858
1241
|
// src/telemetry-server/tracker.ts
|
|
859
|
-
import { randomUUID
|
|
1242
|
+
import { randomUUID } from "crypto";
|
|
860
1243
|
|
|
861
1244
|
// src/telemetry-server/transport.ts
|
|
862
1245
|
import { PostHog } from "posthog-node";
|
|
@@ -1018,7 +1401,7 @@ var WarehouseTracker = class {
|
|
|
1018
1401
|
service_version: this.common.serviceVersion,
|
|
1019
1402
|
...this.common.buildId ? { build_id: this.common.buildId } : {},
|
|
1020
1403
|
// timing / tracing
|
|
1021
|
-
event_id:
|
|
1404
|
+
event_id: randomUUID(),
|
|
1022
1405
|
...input.requestId ? { request_id: input.requestId } : {},
|
|
1023
1406
|
// (trace_id/span_id omitted — no OpenTelemetry; spec forbids fabricating)
|
|
1024
1407
|
// non-user actors: don't create a PostHog person
|
|
@@ -1283,8 +1666,8 @@ function startHttpServer() {
|
|
|
1283
1666
|
common: {
|
|
1284
1667
|
serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
|
|
1285
1668
|
// matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
|
|
1286
|
-
serviceVersion: "0.3.
|
|
1287
|
-
buildId: "
|
|
1669
|
+
serviceVersion: "0.3.11",
|
|
1670
|
+
buildId: "516b086",
|
|
1288
1671
|
// mcp tsup TODO: inject git short SHA (like CLI)
|
|
1289
1672
|
region: process.env.PLAUD_REGION ?? "US",
|
|
1290
1673
|
env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
|
|
@@ -1374,7 +1757,10 @@ function startHttpServer() {
|
|
|
1374
1757
|
server_url: process.env.PLAUD_SERVER_URL ?? "(default:localhost)",
|
|
1375
1758
|
oauth_debug_logs: OAUTH_DEBUG_LOGS,
|
|
1376
1759
|
cimd_enabled: CIMD_ENABLED
|
|
1377
|
-
}
|
|
1760
|
+
},
|
|
1761
|
+
// Size of the token-verification cache, to confirm it is actually in
|
|
1762
|
+
// effect. Counts only — no tokens or user identifiers.
|
|
1763
|
+
token_verifier: provider.verifierStats
|
|
1378
1764
|
});
|
|
1379
1765
|
});
|
|
1380
1766
|
app.get("/", (_req, res) => {
|
|
@@ -1382,8 +1768,8 @@ function startHttpServer() {
|
|
|
1382
1768
|
});
|
|
1383
1769
|
if (process.env.PLAUD_CALLBACK_URL) {
|
|
1384
1770
|
app.get(CALLBACK_PATH, (req, res) => {
|
|
1385
|
-
const code = req.query["code"];
|
|
1386
|
-
const state = req.query["state"];
|
|
1771
|
+
const code = queryValue(req.query["code"]);
|
|
1772
|
+
const state = queryValue(req.query["state"]);
|
|
1387
1773
|
if (!code || !state) {
|
|
1388
1774
|
logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
|
|
1389
1775
|
res.status(400).send("Missing code or state");
|
|
@@ -1393,7 +1779,7 @@ function startHttpServer() {
|
|
|
1393
1779
|
});
|
|
1394
1780
|
}
|
|
1395
1781
|
app.use((req, res, next) => {
|
|
1396
|
-
const reqId = req.headers["x-request-id"] ??
|
|
1782
|
+
const reqId = req.headers["x-request-id"] ?? randomUUID2();
|
|
1397
1783
|
const startMs = Date.now();
|
|
1398
1784
|
res.locals["reqId"] = reqId;
|
|
1399
1785
|
res.on("finish", () => {
|
|
@@ -1545,7 +1931,7 @@ function startHttpServer() {
|
|
|
1545
1931
|
requireBearerAuth({ verifier: provider, resourceMetadataUrl: protectedResourceMetadataUrl }),
|
|
1546
1932
|
async (req, res) => {
|
|
1547
1933
|
const token = req.auth.token;
|
|
1548
|
-
const reqId = res.locals["reqId"] ??
|
|
1934
|
+
const reqId = res.locals["reqId"] ?? randomUUID2();
|
|
1549
1935
|
const reqLog = logger.child({ req_id: reqId });
|
|
1550
1936
|
const startMs = Date.now();
|
|
1551
1937
|
reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
|
|
@@ -1556,7 +1942,7 @@ function startHttpServer() {
|
|
|
1556
1942
|
apiBase,
|
|
1557
1943
|
staticToken: token
|
|
1558
1944
|
});
|
|
1559
|
-
const mcpServer = new McpServer({ name: "plaud", version: "0.3.
|
|
1945
|
+
const mcpServer = new McpServer({ name: "plaud", version: "0.3.11" });
|
|
1560
1946
|
registerTools(mcpServer, client, warehouseToolHooks);
|
|
1561
1947
|
const transport = new StreamableHTTPServerTransport({
|
|
1562
1948
|
sessionIdGenerator: void 0,
|
|
@@ -1594,8 +1980,8 @@ function startHttpServer() {
|
|
|
1594
1980
|
if (!process.env.PLAUD_CALLBACK_URL) {
|
|
1595
1981
|
const callbackApp = express();
|
|
1596
1982
|
callbackApp.get(CALLBACK_PATH, (req, res) => {
|
|
1597
|
-
const code = req.query["code"];
|
|
1598
|
-
const state = req.query["state"];
|
|
1983
|
+
const code = queryValue(req.query["code"]);
|
|
1984
|
+
const state = queryValue(req.query["state"]);
|
|
1599
1985
|
if (!code || !state) {
|
|
1600
1986
|
logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
|
|
1601
1987
|
res.status(400).send("Missing code or state");
|