@plaud-ai/mcp 0.2.2 → 0.2.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.
@@ -0,0 +1,1039 @@
1
+ import {
2
+ logger,
3
+ registerTools
4
+ } from "./chunk-BZOIX6WC.js";
5
+ import {
6
+ PlaudClient
7
+ } from "./chunk-3XRFIJUG.js";
8
+
9
+ // src/http/server.ts
10
+ import express from "express";
11
+ import { createServer } from "http";
12
+ import { randomUUID as randomUUID2 } from "crypto";
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
15
+ import { createOAuthMetadata, mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
16
+ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
17
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
18
+
19
+ // src/http/oauth-provider.ts
20
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
21
+ import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
22
+ import {
23
+ InvalidGrantError,
24
+ InvalidTokenError,
25
+ ServerError as McpServerError
26
+ } from "@modelcontextprotocol/sdk/server/auth/errors.js";
27
+
28
+ // src/http/cimd.ts
29
+ import { lookup as dnsLookup } from "dns/promises";
30
+ import { isIP, isIPv4, isIPv6 } from "net";
31
+ var FETCH_TIMEOUT_MS = 5e3;
32
+ var MAX_DOCUMENT_SIZE_BYTES = 10 * 1024;
33
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
34
+ var CACHE_MAX_ENTRIES = 1e3;
35
+ var FORBIDDEN_AUTH_METHODS = /* @__PURE__ */ new Set([
36
+ "client_secret_post",
37
+ "client_secret_basic",
38
+ "client_secret_jwt"
39
+ ]);
40
+ function isValidCimdUrl(value) {
41
+ if (typeof value !== "string" || !URL.canParse(value)) return false;
42
+ const url = new URL(value);
43
+ if (url.protocol !== "https:") return false;
44
+ if (url.username || url.password) return false;
45
+ if (url.hash) return false;
46
+ if (!url.pathname || url.pathname === "/") return false;
47
+ if (url.pathname.split("/").some((seg) => seg === "." || seg === "..")) return false;
48
+ return true;
49
+ }
50
+ function isPrivateIp(ip) {
51
+ if (isIPv4(ip)) {
52
+ const parts = ip.split(".").map((p) => Number(p));
53
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return false;
54
+ const [a, b] = parts;
55
+ if (a === 0) return true;
56
+ if (a === 10) return true;
57
+ if (a === 127) return true;
58
+ if (a === 169 && b === 254) return true;
59
+ if (a === 172 && b >= 16 && b <= 31) return true;
60
+ if (a === 192 && b === 168) return true;
61
+ if (a >= 224) return true;
62
+ return false;
63
+ }
64
+ if (isIPv6(ip)) {
65
+ const lower = ip.toLowerCase();
66
+ if (lower === "::" || lower === "::1") return true;
67
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
68
+ if (lower.startsWith("fe80:")) return true;
69
+ if (lower.startsWith("ff")) return true;
70
+ if (lower.startsWith("::ffff:")) {
71
+ const v4 = lower.slice("::ffff:".length);
72
+ if (isIPv4(v4)) return isPrivateIp(v4);
73
+ }
74
+ return false;
75
+ }
76
+ return false;
77
+ }
78
+ var CimdLoader = class {
79
+ cache = /* @__PURE__ */ new Map();
80
+ allowPrivateIp;
81
+ fetchImpl;
82
+ lookupImpl;
83
+ ttlMs;
84
+ constructor(options = {}) {
85
+ this.allowPrivateIp = options.allowPrivateIp ?? false;
86
+ this.fetchImpl = options.fetchImpl ?? fetch;
87
+ this.lookupImpl = options.lookupImpl ?? dnsLookup;
88
+ this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
89
+ }
90
+ /**
91
+ * Resolve a CIMD client_id URL into an OAuthClientInformationFull, fetching
92
+ * and validating the metadata document if necessary. Returns undefined on
93
+ * any failure (logged at warn level).
94
+ */
95
+ async load(clientIdUrl) {
96
+ if (!isValidCimdUrl(clientIdUrl)) {
97
+ logger.warn({ event: "cimd_invalid_url", client_id_url: clientIdUrl });
98
+ return void 0;
99
+ }
100
+ const cached = this.cache.get(clientIdUrl);
101
+ if (cached && cached.expiresAt > Date.now()) {
102
+ return cached.client;
103
+ }
104
+ if (cached) this.cache.delete(clientIdUrl);
105
+ if (!this.allowPrivateIp) {
106
+ const safe = await this.checkHostAllowed(new URL(clientIdUrl).hostname);
107
+ if (!safe) return void 0;
108
+ }
109
+ const client = await this.fetchAndValidate(clientIdUrl);
110
+ if (!client) return void 0;
111
+ this.put(clientIdUrl, client);
112
+ logger.info({
113
+ event: "cimd_client_loaded",
114
+ client_id_url: clientIdUrl,
115
+ client_name: client.client_name ?? null,
116
+ redirect_uri_count: client.redirect_uris.length
117
+ });
118
+ return client;
119
+ }
120
+ async checkHostAllowed(hostname) {
121
+ if (isIP(hostname) !== 0) {
122
+ logger.warn({ event: "cimd_ip_literal_blocked", hostname });
123
+ return false;
124
+ }
125
+ try {
126
+ const results = await this.lookupImpl(hostname, { all: true });
127
+ for (const r of results) {
128
+ if (isPrivateIp(r.address)) {
129
+ logger.warn({ event: "cimd_private_ip_blocked", hostname, ip: r.address });
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ } catch (err) {
135
+ logger.warn({
136
+ event: "cimd_dns_failed",
137
+ hostname,
138
+ error: err instanceof Error ? err.message : String(err)
139
+ });
140
+ return false;
141
+ }
142
+ }
143
+ async fetchAndValidate(clientIdUrl) {
144
+ const ctrl = new AbortController();
145
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
146
+ try {
147
+ const res = await this.fetchImpl(clientIdUrl, {
148
+ method: "GET",
149
+ headers: { Accept: "application/json" },
150
+ signal: ctrl.signal,
151
+ redirect: "error"
152
+ // CIMD URL identifies the client; redirects not allowed
153
+ });
154
+ if (!res.ok) {
155
+ logger.warn({ event: "cimd_fetch_non_ok", client_id_url: clientIdUrl, status: res.status });
156
+ return void 0;
157
+ }
158
+ const declared = Number(res.headers.get("content-length") ?? "0");
159
+ if (declared > MAX_DOCUMENT_SIZE_BYTES) {
160
+ logger.warn({
161
+ event: "cimd_document_too_large",
162
+ client_id_url: clientIdUrl,
163
+ content_length: declared
164
+ });
165
+ return void 0;
166
+ }
167
+ const text = await res.text();
168
+ if (text.length > MAX_DOCUMENT_SIZE_BYTES) {
169
+ logger.warn({
170
+ event: "cimd_document_too_large",
171
+ client_id_url: clientIdUrl,
172
+ actual_size: text.length
173
+ });
174
+ return void 0;
175
+ }
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(text);
179
+ } catch (err) {
180
+ logger.warn({
181
+ event: "cimd_invalid_json",
182
+ client_id_url: clientIdUrl,
183
+ error: err instanceof Error ? err.message : String(err)
184
+ });
185
+ return void 0;
186
+ }
187
+ return validateMetadata(parsed, clientIdUrl);
188
+ } catch (err) {
189
+ logger.warn({
190
+ event: "cimd_fetch_error",
191
+ client_id_url: clientIdUrl,
192
+ error: err instanceof Error ? err.message : String(err)
193
+ });
194
+ return void 0;
195
+ } finally {
196
+ clearTimeout(timer);
197
+ }
198
+ }
199
+ put(key, client) {
200
+ if (this.cache.size >= CACHE_MAX_ENTRIES) {
201
+ const oldest = this.cache.keys().next().value;
202
+ if (oldest !== void 0) this.cache.delete(oldest);
203
+ }
204
+ this.cache.set(key, { client, expiresAt: Date.now() + this.ttlMs });
205
+ }
206
+ };
207
+ function validateMetadata(parsed, expectedClientIdUrl) {
208
+ if (!parsed || typeof parsed !== "object") {
209
+ logger.warn({ event: "cimd_invalid_shape", client_id_url: expectedClientIdUrl });
210
+ return void 0;
211
+ }
212
+ const m = parsed;
213
+ if (m["client_id"] !== expectedClientIdUrl) {
214
+ logger.warn({
215
+ event: "cimd_client_id_mismatch",
216
+ client_id_url: expectedClientIdUrl,
217
+ document_client_id: typeof m["client_id"] === "string" ? m["client_id"] : null
218
+ });
219
+ return void 0;
220
+ }
221
+ const redirects = m["redirect_uris"];
222
+ if (!Array.isArray(redirects) || redirects.length === 0 || !redirects.every((u) => typeof u === "string")) {
223
+ logger.warn({ event: "cimd_invalid_redirect_uris", client_id_url: expectedClientIdUrl });
224
+ return void 0;
225
+ }
226
+ const authMethod = m["token_endpoint_auth_method"];
227
+ if (typeof authMethod === "string" && FORBIDDEN_AUTH_METHODS.has(authMethod)) {
228
+ logger.warn({
229
+ event: "cimd_forbidden_auth_method",
230
+ client_id_url: expectedClientIdUrl,
231
+ token_endpoint_auth_method: authMethod
232
+ });
233
+ return void 0;
234
+ }
235
+ const optString = (key) => typeof m[key] === "string" ? m[key] : void 0;
236
+ const optStringArray = (key) => {
237
+ const v = m[key];
238
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
239
+ };
240
+ return {
241
+ client_id: expectedClientIdUrl,
242
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
243
+ redirect_uris: redirects,
244
+ token_endpoint_auth_method: typeof authMethod === "string" ? authMethod : "none",
245
+ grant_types: optStringArray("grant_types"),
246
+ response_types: optStringArray("response_types"),
247
+ client_name: optString("client_name"),
248
+ client_uri: optString("client_uri"),
249
+ logo_uri: optString("logo_uri"),
250
+ scope: optString("scope"),
251
+ contacts: optStringArray("contacts"),
252
+ tos_uri: optString("tos_uri"),
253
+ policy_uri: optString("policy_uri"),
254
+ jwks_uri: optString("jwks_uri"),
255
+ software_id: optString("software_id"),
256
+ software_version: optString("software_version"),
257
+ software_statement: optString("software_statement")
258
+ };
259
+ }
260
+
261
+ // src/http/oauth-provider.ts
262
+ var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
263
+ var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
264
+ var STATELESS_CODE_PREFIX = "pc1_";
265
+ var AUTHORIZATION_CODE_TTL_MS = 10 * 60 * 1e3;
266
+ function urlParts(value) {
267
+ if (!value || !URL.canParse(value)) {
268
+ return { host: null, path: null };
269
+ }
270
+ const url = new URL(value);
271
+ return { host: url.host, path: url.pathname };
272
+ }
273
+ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
274
+ _plaudClientId;
275
+ _plaudClientSecret;
276
+ _plaudTokenUrl;
277
+ _plaudRefreshUrl;
278
+ _plaudApiBase;
279
+ _callbackUrl;
280
+ _debugOAuthLogs;
281
+ _cimdLoader;
282
+ _registeredClients = /* @__PURE__ */ new Map();
283
+ // HMAC secret used to sign DCR-issued client_ids so we can recover them
284
+ // across container restarts without persistent storage. See verifyAndRecover.
285
+ _clientIdSecret;
286
+ // internalState → client redirect, original client state, and resource indicator
287
+ // We generate our own state to track the pending flow regardless of whether the client sent one.
288
+ _pendingStates = /* @__PURE__ */ new Map();
289
+ constructor(options) {
290
+ const authUrl = options.authUrl ?? "https://web.plaud.ai/platform/oauth";
291
+ const tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
292
+ const refreshUrl = options.refreshUrl ?? DEFAULT_REFRESH_URL;
293
+ const apiBase = options.apiBase ?? "https://platform.plaud.ai/developer/api";
294
+ super({
295
+ endpoints: {
296
+ authorizationUrl: authUrl,
297
+ tokenUrl
298
+ },
299
+ verifyAccessToken: async (token) => {
300
+ const client = new PlaudClient({
301
+ clientId: options.clientId,
302
+ clientSecret: "",
303
+ redirectUri: "",
304
+ apiBase,
305
+ staticToken: token
306
+ });
307
+ try {
308
+ const user = await client.getCurrentUser();
309
+ let expiresAt;
310
+ try {
311
+ const payload = JSON.parse(
312
+ Buffer.from(token.split(".")[1], "base64url").toString()
313
+ );
314
+ expiresAt = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + 3600;
315
+ } catch {
316
+ expiresAt = Math.floor(Date.now() / 1e3) + 3600;
317
+ }
318
+ const authInfo = {
319
+ token,
320
+ clientId: String(user.id ?? "unknown"),
321
+ scopes: [],
322
+ expiresAt
323
+ };
324
+ logger.info({ event: "token_verified", client_id: authInfo.clientId, expires_at: expiresAt });
325
+ return authInfo;
326
+ } catch (err) {
327
+ logger.warn({ event: "token_verify_failed", error: String(err) });
328
+ throw new InvalidTokenError("Invalid or expired token");
329
+ }
330
+ },
331
+ getClient: async (id) => this._registeredClients.get(id)
332
+ });
333
+ this._plaudClientId = options.clientId;
334
+ this._plaudClientSecret = options.clientSecret ?? "";
335
+ this._plaudTokenUrl = tokenUrl;
336
+ this._plaudRefreshUrl = refreshUrl;
337
+ this._plaudApiBase = apiBase;
338
+ this._callbackUrl = options.callbackUrl;
339
+ this._debugOAuthLogs = options.debugOAuthLogs ?? false;
340
+ this._cimdLoader = options.cimdLoader;
341
+ const secret = process.env["PLAUD_DCR_HMAC_SECRET"];
342
+ if (secret && secret.length >= 32) {
343
+ this._clientIdSecret = Buffer.from(secret, "utf-8");
344
+ } else {
345
+ this._clientIdSecret = randomBytes(32);
346
+ logger.warn({
347
+ event: "oauth_dcr_secret_missing",
348
+ message: "PLAUD_DCR_HMAC_SECRET unset or too short (<32 chars); using ephemeral per-process secret. Cached client_ids will not survive restarts."
349
+ });
350
+ }
351
+ this.skipLocalPkceValidation = true;
352
+ }
353
+ // Override clientsStore. registerClient signs the issued client_id with HMAC
354
+ // so verifyAndRecover can later resurrect it across container restarts.
355
+ get clientsStore() {
356
+ return {
357
+ getClient: async (id) => {
358
+ if (this._cimdLoader && isValidCimdUrl(id)) {
359
+ return await this._cimdLoader.load(id);
360
+ }
361
+ return this._registeredClients.get(id);
362
+ },
363
+ registerClient: async (client) => {
364
+ const rawId = randomBytes(16).toString("base64url");
365
+ const tokenEndpointAuthMethod = client.token_endpoint_auth_method ?? "none";
366
+ const full = {
367
+ ...client,
368
+ token_endpoint_auth_method: tokenEndpointAuthMethod,
369
+ client_secret: tokenEndpointAuthMethod === "none" ? void 0 : client.client_secret,
370
+ client_secret_expires_at: tokenEndpointAuthMethod === "none" ? void 0 : client.client_secret_expires_at,
371
+ client_id: this.signClientId(rawId),
372
+ client_id_issued_at: Math.floor(Date.now() / 1e3)
373
+ };
374
+ this._registeredClients.set(full.client_id, full);
375
+ if (this._debugOAuthLogs) {
376
+ logger.info({
377
+ event: "oauth_client_registered",
378
+ client_id_len: full.client_id.length,
379
+ client_name: client.client_name ?? null,
380
+ client_uri: client.client_uri ?? null,
381
+ logo_uri: client.logo_uri ?? null,
382
+ redirect_uri_count: full.redirect_uris.length,
383
+ redirect_uri_hosts: [...new Set(full.redirect_uris.map((uri) => urlParts(uri).host).filter(Boolean))],
384
+ redirect_uri_paths: full.redirect_uris.map((uri) => urlParts(uri).path).filter(Boolean),
385
+ token_endpoint_auth_method: full.token_endpoint_auth_method ?? null,
386
+ grant_types: full.grant_types ?? null,
387
+ response_types: full.response_types ?? null,
388
+ has_client_secret: !!full.client_secret,
389
+ client_secret_expires_at: full.client_secret_expires_at ?? null,
390
+ registered_clients_count: this._registeredClients.size
391
+ });
392
+ }
393
+ return full;
394
+ }
395
+ };
396
+ }
397
+ // Sign a raw id so the resulting client_id is `<rawId>.<base64url(hmac)>`.
398
+ // Truncated to 16 bytes (128 bits) — plenty for non-cryptographic forgery
399
+ // resistance, keeps the client_id short.
400
+ signClientId(rawId) {
401
+ const sig = createHmac("sha256", this._clientIdSecret).update(rawId).digest().subarray(0, 16);
402
+ return `${rawId}.${sig.toString("base64url")}`;
403
+ }
404
+ // Verify a previously-issued client_id. Returns true only for ids whose HMAC
405
+ // signature matches our secret — i.e. ids this server (or its predecessor with
406
+ // the same PLAUD_DCR_HMAC_SECRET) issued via registerClient.
407
+ verifyClientIdSignature(signedId) {
408
+ const idx = signedId.lastIndexOf(".");
409
+ if (idx <= 0 || idx >= signedId.length - 1) return false;
410
+ const rawId = signedId.slice(0, idx);
411
+ const sigPart = signedId.slice(idx + 1);
412
+ const expected = createHmac("sha256", this._clientIdSecret).update(rawId).digest().subarray(0, 16);
413
+ let actual;
414
+ try {
415
+ actual = Buffer.from(sigPart, "base64url");
416
+ } catch {
417
+ return false;
418
+ }
419
+ if (actual.length !== expected.length) return false;
420
+ return timingSafeEqual(actual, expected);
421
+ }
422
+ authorizationCodeKey() {
423
+ return createHmac("sha256", this._clientIdSecret).update("plaud-mcp-authorization-code").digest();
424
+ }
425
+ encodeAuthorizationCode(payload) {
426
+ const iv = randomBytes(12);
427
+ const cipher = createCipheriv("aes-256-gcm", this.authorizationCodeKey(), iv);
428
+ const plaintext = Buffer.from(JSON.stringify({
429
+ upstreamCode: payload.upstreamCode,
430
+ upstreamState: payload.upstreamState,
431
+ resource: payload.resource,
432
+ expiresAt: Date.now() + AUTHORIZATION_CODE_TTL_MS
433
+ }), "utf8");
434
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
435
+ const tag = cipher.getAuthTag();
436
+ return `${STATELESS_CODE_PREFIX}${Buffer.concat([iv, tag, ciphertext]).toString("base64url")}`;
437
+ }
438
+ decodeAuthorizationCode(code) {
439
+ if (!code.startsWith(STATELESS_CODE_PREFIX)) return null;
440
+ try {
441
+ const encoded = code.slice(STATELESS_CODE_PREFIX.length);
442
+ const data = Buffer.from(encoded, "base64url");
443
+ if (data.length <= 28) return null;
444
+ const iv = data.subarray(0, 12);
445
+ const tag = data.subarray(12, 28);
446
+ const ciphertext = data.subarray(28);
447
+ const decipher = createDecipheriv("aes-256-gcm", this.authorizationCodeKey(), iv);
448
+ decipher.setAuthTag(tag);
449
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
450
+ const payload = JSON.parse(plaintext);
451
+ if (typeof payload.upstreamCode !== "string" || typeof payload.upstreamState !== "string" || payload.resource !== void 0 && typeof payload.resource !== "string" || typeof payload.expiresAt !== "number") {
452
+ return null;
453
+ }
454
+ if (Date.now() > payload.expiresAt) {
455
+ return null;
456
+ }
457
+ return {
458
+ upstreamCode: payload.upstreamCode,
459
+ upstreamState: payload.upstreamState,
460
+ resource: payload.resource
461
+ };
462
+ } catch {
463
+ return null;
464
+ }
465
+ }
466
+ // Recover a client_id that the registry doesn't know about. Directory clients
467
+ // (OpenAI Apps, Claude directory) cache the client_id they got from /register;
468
+ // when this stateless container restarts, the in-memory map is empty and the
469
+ // SDK auth handlers reject the cached client_id with InvalidClientError.
470
+ // We synthesize an entry on demand — but only for client_ids whose HMAC
471
+ // signature verifies against our secret, so this server can't be coaxed into
472
+ // accepting forged ids. Real auth still happens upstream at Plaud.
473
+ // Returns true if the caller should proceed (client is now in the registry).
474
+ verifyAndRecover(clientId, redirectUri) {
475
+ const existing = this._registeredClients.get(clientId);
476
+ if (existing) {
477
+ if (redirectUri && !existing.redirect_uris.includes(redirectUri)) {
478
+ existing.redirect_uris = [...existing.redirect_uris, redirectUri];
479
+ }
480
+ return true;
481
+ }
482
+ if (!this.verifyClientIdSignature(clientId)) {
483
+ return false;
484
+ }
485
+ this._registeredClients.set(clientId, {
486
+ client_id: clientId,
487
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
488
+ redirect_uris: redirectUri ? [redirectUri] : [],
489
+ grant_types: ["authorization_code", "refresh_token"],
490
+ response_types: ["code"],
491
+ token_endpoint_auth_method: "none",
492
+ client_name: "auto-recovered client"
493
+ });
494
+ logger.info({ event: "oauth_client_recovered", client_id: clientId, redirect_uri: redirectUri ?? null });
495
+ return true;
496
+ }
497
+ /**
498
+ * Redirect to Plaud using our own registered callback URL.
499
+ * Store the client's original redirect_uri keyed by state so we can forward after Plaud calls back.
500
+ */
501
+ async authorize(_client, params, res) {
502
+ const internalState = randomUUID();
503
+ this._pendingStates.set(internalState, {
504
+ clientRedirectUri: params.redirectUri,
505
+ originalState: params.state,
506
+ resource: params.resource?.href
507
+ });
508
+ const redirectParts = urlParts(params.redirectUri);
509
+ logger.info({
510
+ event: "oauth_authorize_start",
511
+ internal_state: internalState,
512
+ redirect_uri: params.redirectUri,
513
+ ...this._debugOAuthLogs ? {
514
+ redirect_uri_host: redirectParts.host,
515
+ redirect_uri_path: redirectParts.path,
516
+ has_original_state: !!params.state,
517
+ has_resource: !!params.resource,
518
+ resource: params.resource?.href ?? null,
519
+ pending_states_count: this._pendingStates.size
520
+ } : {}
521
+ });
522
+ const targetUrl = new URL(this._endpoints.authorizationUrl);
523
+ const searchParams = new URLSearchParams({
524
+ client_id: this._plaudClientId,
525
+ response_type: "code",
526
+ redirect_uri: this._callbackUrl,
527
+ code_challenge: params.codeChallenge,
528
+ code_challenge_method: "S256",
529
+ state: internalState
530
+ // always send our internal state to Plaud
531
+ });
532
+ if (params.scopes?.length) searchParams.set("scope", params.scopes.join(" "));
533
+ if (_client.client_name) searchParams.set("mcp_display_name", _client.client_name);
534
+ if (_client.redirect_uris?.[0]) searchParams.set("mcp_redirect_uri", _client.redirect_uris[0]);
535
+ if (_client.logo_uri) searchParams.set("mcp_logo_uri", _client.logo_uri);
536
+ if (_client.client_uri) searchParams.set("mcp_client_uri", _client.client_uri);
537
+ targetUrl.search = searchParams.toString();
538
+ logger.info({ event: "oauth_authorize_redirect", url: targetUrl.toString(), scopes: params.scopes ?? [] });
539
+ res.redirect(targetUrl.toString());
540
+ }
541
+ /**
542
+ * Called when Plaud redirects to our /oauth/callback.
543
+ * Looks up the original client redirect_uri and forwards the code+state to it.
544
+ */
545
+ handleCallback(code, state, res) {
546
+ const pending = this._pendingStates.get(state);
547
+ if (!pending) {
548
+ logger.warn({ event: "oauth_callback_unknown_state", state });
549
+ res.status(400).send("Unknown state \u2014 authorization request not found");
550
+ return;
551
+ }
552
+ this._pendingStates.delete(state);
553
+ logger.info({ event: "oauth_callback_received", internal_state: state });
554
+ const pendingCode = {
555
+ upstreamCode: code,
556
+ upstreamState: state,
557
+ resource: pending.resource
558
+ };
559
+ const clientCode = this.encodeAuthorizationCode(pendingCode);
560
+ const target = new URL(pending.clientRedirectUri);
561
+ target.searchParams.set("code", clientCode);
562
+ if (pending.originalState) {
563
+ target.searchParams.set("state", pending.originalState);
564
+ }
565
+ const redirectParts = urlParts(pending.clientRedirectUri);
566
+ logger.info({
567
+ event: "oauth_callback_redirect",
568
+ internal_state: state,
569
+ redirect_uri: pending.clientRedirectUri,
570
+ has_original_state: !!pending.originalState,
571
+ original_state_len: pending.originalState?.length ?? 0,
572
+ upstream_code_len: code.length,
573
+ client_code_len: clientCode.length,
574
+ stateless_code: true,
575
+ redirect_url_len: target.toString().length,
576
+ ...this._debugOAuthLogs ? {
577
+ redirect_uri_host: redirectParts.host,
578
+ redirect_uri_path: redirectParts.path,
579
+ redirect_has_existing_query: pending.clientRedirectUri.includes("?"),
580
+ redirect_has_code: target.searchParams.has("code"),
581
+ redirect_has_state: target.searchParams.has("state")
582
+ } : {}
583
+ });
584
+ res.redirect(target.toString());
585
+ }
586
+ // Override to use PKCE public-client flow — no Basic auth, client_id sent in body.
587
+ async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, _redirectUri, resource) {
588
+ const pendingCode = this.decodeAuthorizationCode(authorizationCode);
589
+ if (!pendingCode) {
590
+ logger.warn({ event: "oauth_authorization_code_unknown", code_len: authorizationCode.length });
591
+ throw new InvalidGrantError("Unknown or expired authorization code");
592
+ }
593
+ if (pendingCode.resource && resource && resource.href !== pendingCode.resource) {
594
+ logger.warn({
595
+ event: "oauth_resource_mismatch",
596
+ expected_resource: pendingCode.resource,
597
+ received_resource: resource.href
598
+ });
599
+ throw new InvalidGrantError("Mismatched resource");
600
+ }
601
+ const body = {
602
+ grant_type: "authorization_code",
603
+ client_id: this._plaudClientId,
604
+ code: pendingCode.upstreamCode,
605
+ redirect_uri: this._callbackUrl
606
+ };
607
+ body.state = pendingCode.upstreamState;
608
+ if (codeVerifier) body.code_verifier = codeVerifier;
609
+ const basicAuth = Buffer.from(`${this._plaudClientId}:${this._plaudClientSecret}`).toString("base64");
610
+ logger.info({
611
+ event: "oauth_token_exchange_attempt",
612
+ token_url: this._plaudTokenUrl,
613
+ client_id: this._plaudClientId,
614
+ has_secret: !!this._plaudClientSecret,
615
+ has_code_verifier: !!body.code_verifier,
616
+ redirect_uri: body.redirect_uri
617
+ });
618
+ const fetchRes = await fetch(this._plaudTokenUrl, {
619
+ method: "POST",
620
+ headers: {
621
+ "Content-Type": "application/x-www-form-urlencoded",
622
+ Accept: "application/json",
623
+ Authorization: `Basic ${basicAuth}`
624
+ },
625
+ body: new URLSearchParams(body)
626
+ });
627
+ if (!fetchRes.ok) {
628
+ let text;
629
+ try {
630
+ text = await fetchRes.text();
631
+ } catch {
632
+ text = "<unreadable>";
633
+ }
634
+ logger.error({ event: "oauth_token_exchange_failed", status: fetchRes.status, body: text.slice(0, 500) });
635
+ if (fetchRes.status >= 500) {
636
+ throw new McpServerError(`Upstream token endpoint error: ${fetchRes.status} ${text.slice(0, 200)}`);
637
+ }
638
+ throw new InvalidGrantError(`Plaud token exchange: ${fetchRes.status} ${text.slice(0, 200)}`);
639
+ }
640
+ let data;
641
+ try {
642
+ data = await fetchRes.json();
643
+ } catch (err) {
644
+ logger.error({ event: "oauth_token_json_parse_failed", error: String(err) });
645
+ throw new McpServerError("Token endpoint returned non-JSON response");
646
+ }
647
+ logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
648
+ return {
649
+ access_token: data.access_token,
650
+ token_type: data.token_type ?? "Bearer",
651
+ refresh_token: data.refresh_token,
652
+ expires_in: data.expires_in
653
+ };
654
+ }
655
+ // Override refresh too: the SDK proxy implementation would forward the local
656
+ // DCR client_id and RFC 8707 resource to Plaud, but Plaud uses its dedicated
657
+ // refresh endpoint and does not support resource indicators.
658
+ async exchangeRefreshToken(_client, refreshToken, scopes, resource) {
659
+ logger.info({
660
+ event: "oauth_token_refresh_attempt",
661
+ token_url: this._plaudRefreshUrl,
662
+ has_scope: !!scopes?.length,
663
+ has_resource: !!resource,
664
+ ...this._debugOAuthLogs ? { resource: resource?.href ?? null } : {}
665
+ });
666
+ const fetchRes = await fetch(this._plaudRefreshUrl, {
667
+ method: "POST",
668
+ headers: {
669
+ "Content-Type": "application/x-www-form-urlencoded",
670
+ Accept: "application/json"
671
+ },
672
+ body: new URLSearchParams({ refresh_token: refreshToken })
673
+ });
674
+ if (!fetchRes.ok) {
675
+ let text;
676
+ try {
677
+ text = await fetchRes.text();
678
+ } catch {
679
+ text = "<unreadable>";
680
+ }
681
+ logger.error({ event: "oauth_token_refresh_failed", status: fetchRes.status, body: text.slice(0, 500) });
682
+ if (fetchRes.status >= 500) {
683
+ throw new McpServerError(`Upstream refresh endpoint error: ${fetchRes.status} ${text.slice(0, 200)}`);
684
+ }
685
+ throw new InvalidGrantError(`Plaud token refresh: ${fetchRes.status} ${text.slice(0, 200)}`);
686
+ }
687
+ let data;
688
+ try {
689
+ data = await fetchRes.json();
690
+ } catch (err) {
691
+ logger.error({ event: "oauth_refresh_json_parse_failed", error: String(err) });
692
+ throw new McpServerError("Refresh endpoint returned non-JSON response");
693
+ }
694
+ logger.info({
695
+ event: "oauth_token_refresh_ok",
696
+ has_refresh_token: !!data.refresh_token,
697
+ ignored_scope: !!scopes?.length,
698
+ ignored_resource: !!resource
699
+ });
700
+ return {
701
+ access_token: data.access_token,
702
+ token_type: data.token_type ?? "Bearer",
703
+ refresh_token: data.refresh_token ?? refreshToken,
704
+ expires_in: data.expires_in
705
+ };
706
+ }
707
+ };
708
+
709
+ // src/http/server.ts
710
+ var HTTP_PORT = Number(process.env.PLAUD_HTTP_PORT ?? 3e3);
711
+ var HTTP_HOST = process.env.PLAUD_HTTP_HOST ?? "0.0.0.0";
712
+ var CALLBACK_PORT = 8199;
713
+ var CALLBACK_PATH = "/auth/callback";
714
+ var CALLBACK_URL = process.env.PLAUD_CALLBACK_URL ?? `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
715
+ var DEFAULT_ALLOWED_ORIGINS = [
716
+ "https://claude.ai",
717
+ "https://claude.com",
718
+ "https://chatgpt.com",
719
+ "https://chat.openai.com",
720
+ "https://platform.openai.com"
721
+ ];
722
+ var ALLOWED_ORIGINS = (process.env.PLAUD_ALLOWED_ORIGINS ?? DEFAULT_ALLOWED_ORIGINS.join(",")).split(",").map((s) => s.trim()).filter(Boolean);
723
+ var OPENAI_APPS_CHALLENGE_TOKEN = "bBlB7N7fF7YaQnONtEQNAGP7SLaqpJT3a3DS5Zv1F8s";
724
+ var OAUTH_DEBUG_LOGS = !["0", "false", "no", "off"].includes(
725
+ (process.env.PLAUD_OAUTH_DEBUG_LOGS ?? "").toLowerCase()
726
+ );
727
+ var CIMD_ENABLED = ["1", "true", "yes", "on"].includes(
728
+ (process.env.PLAUD_CIMD_ENABLED ?? "").toLowerCase()
729
+ );
730
+ var CIMD_ALLOW_PRIVATE_IP = ["1", "true", "yes", "on"].includes(
731
+ (process.env.PLAUD_CIMD_ALLOW_PRIVATE_IP ?? "").toLowerCase()
732
+ );
733
+ function queryValue(value) {
734
+ return typeof value === "string" ? value : void 0;
735
+ }
736
+ function arrayValue(value) {
737
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
738
+ }
739
+ function urlParts2(value) {
740
+ if (!value || !URL.canParse(value)) {
741
+ return { host: null, path: null };
742
+ }
743
+ const url = new URL(value);
744
+ return { host: url.host, path: url.pathname };
745
+ }
746
+ function basicAuthCredentials(value) {
747
+ if (typeof value !== "string" || !value.startsWith("Basic ")) {
748
+ return null;
749
+ }
750
+ try {
751
+ const decoded = Buffer.from(value.slice("Basic ".length), "base64").toString("utf8");
752
+ const separator = decoded.indexOf(":");
753
+ if (separator < 0) return null;
754
+ return {
755
+ clientId: decoded.slice(0, separator),
756
+ clientSecret: decoded.slice(separator + 1) || void 0
757
+ };
758
+ } catch {
759
+ return null;
760
+ }
761
+ }
762
+ function isDiagnosticPath(path) {
763
+ return path === "/mcp" || path === "/register" || path === "/authorize" || path === "/token" || path.startsWith("/.well-known/");
764
+ }
765
+ function startHttpServer() {
766
+ const clientId = process.env.PLAUD_MCP_CLIENT_ID ?? "client_37d250cb-50f8-4af1-8cd6-bc6711c5d684";
767
+ const clientSecret = process.env.PLAUD_MCP_CLIENT_SECRET;
768
+ const apiBase = process.env.PLAUD_API_BASE;
769
+ const serverUrl = process.env.PLAUD_SERVER_URL ?? `http://localhost:${HTTP_PORT}`;
770
+ const cimdLoader = CIMD_ENABLED ? new CimdLoader({ allowPrivateIp: CIMD_ALLOW_PRIVATE_IP }) : void 0;
771
+ if (CIMD_ENABLED) {
772
+ logger.info({
773
+ event: "cimd_enabled",
774
+ allow_private_ip: CIMD_ALLOW_PRIVATE_IP
775
+ });
776
+ }
777
+ const provider = new PlaudOAuthProvider({
778
+ clientId,
779
+ clientSecret,
780
+ callbackUrl: CALLBACK_URL,
781
+ authUrl: process.env.PLAUD_AUTH_URL,
782
+ tokenUrl: process.env.PLAUD_TOKEN_URL,
783
+ refreshUrl: process.env.PLAUD_REFRESH_URL,
784
+ apiBase,
785
+ debugOAuthLogs: OAUTH_DEBUG_LOGS,
786
+ cimdLoader
787
+ });
788
+ const issuerUrl = new URL(serverUrl);
789
+ const trimmedServerUrl = serverUrl.replace(/\/$/, "");
790
+ const mcpResourceUrl = `${trimmedServerUrl}/mcp`;
791
+ const protectedResourceMetadataUrl = `${trimmedServerUrl}/.well-known/oauth-protected-resource/mcp`;
792
+ const protectedResourceMetadata = {
793
+ resource: mcpResourceUrl,
794
+ authorization_servers: [issuerUrl.href],
795
+ resource_name: "Plaud MCP Server",
796
+ bearer_methods_supported: ["header"]
797
+ };
798
+ const oauthMetadata = {
799
+ ...createOAuthMetadata({ provider, issuerUrl, baseUrl: issuerUrl }),
800
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
801
+ // Only advertise CIMD support when actually enabled. Advertising while the
802
+ // backing fetch path is disabled would mislead clients into using CIMD and
803
+ // failing.
804
+ ...CIMD_ENABLED ? { client_id_metadata_document_supported: true } : {}
805
+ };
806
+ const app = createMcpExpressApp({ host: HTTP_HOST });
807
+ app.set("trust proxy", 1);
808
+ app.get("/.well-known/openai-apps-challenge", (_req, res) => {
809
+ res.type("text/plain").send(OPENAI_APPS_CHALLENGE_TOKEN);
810
+ });
811
+ app.get("/.well-known/oauth-protected-resource", (_req, res) => {
812
+ res.json(protectedResourceMetadata);
813
+ });
814
+ app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
815
+ res.json(protectedResourceMetadata);
816
+ });
817
+ app.get("/.well-known/oauth-authorization-server", (_req, res) => {
818
+ res.json(oauthMetadata);
819
+ });
820
+ app.get("/.well-known/openid-configuration", (_req, res) => {
821
+ res.json(oauthMetadata);
822
+ });
823
+ app.get("/health", (_req, res) => {
824
+ res.json({
825
+ status: "ok",
826
+ uptime_s: Math.floor(process.uptime()),
827
+ config: {
828
+ has_client_id: !!process.env.PLAUD_MCP_CLIENT_ID,
829
+ has_client_secret: !!process.env.PLAUD_MCP_CLIENT_SECRET,
830
+ callback_url: process.env.PLAUD_CALLBACK_URL ?? "(default:localhost)",
831
+ server_url: process.env.PLAUD_SERVER_URL ?? "(default:localhost)",
832
+ oauth_debug_logs: OAUTH_DEBUG_LOGS,
833
+ cimd_enabled: CIMD_ENABLED
834
+ }
835
+ });
836
+ });
837
+ if (process.env.PLAUD_CALLBACK_URL) {
838
+ app.get(CALLBACK_PATH, (req, res) => {
839
+ const code = req.query["code"];
840
+ const state = req.query["state"];
841
+ if (!code || !state) {
842
+ logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
843
+ res.status(400).send("Missing code or state");
844
+ return;
845
+ }
846
+ provider.handleCallback(code, state, res);
847
+ });
848
+ }
849
+ app.use((req, res, next) => {
850
+ const reqId = req.headers["x-request-id"] ?? randomUUID2();
851
+ const startMs = Date.now();
852
+ res.locals["reqId"] = reqId;
853
+ res.on("finish", () => {
854
+ if (OAUTH_DEBUG_LOGS && isDiagnosticPath(req.path)) {
855
+ logger.info({
856
+ event: "http_response",
857
+ req_id: reqId,
858
+ method: req.method,
859
+ path: req.path,
860
+ status_code: res.statusCode,
861
+ duration_ms: Date.now() - startMs
862
+ });
863
+ }
864
+ });
865
+ logger.info({ event: "http_request", req_id: reqId, method: req.method, path: req.url, user_agent: req.headers["user-agent"] ?? null });
866
+ next();
867
+ });
868
+ app.use("/mcp", (req, res, next) => {
869
+ const origin = req.headers["origin"];
870
+ if (origin && !ALLOWED_ORIGINS.includes(origin)) {
871
+ logger.warn({ event: "origin_rejected", origin, req_id: res.locals["reqId"] });
872
+ res.status(403).json({ error: "forbidden_origin", origin });
873
+ return;
874
+ }
875
+ next();
876
+ });
877
+ app.post("/token", express.urlencoded({ extended: false }), (req, res, next) => {
878
+ const basicAuth = basicAuthCredentials(req.headers.authorization);
879
+ if (basicAuth) {
880
+ req.body ??= {};
881
+ req.body.client_id ??= basicAuth.clientId;
882
+ req.body.client_secret ??= basicAuth.clientSecret;
883
+ }
884
+ const resource = queryValue(req.body?.resource);
885
+ const resourceParts = urlParts2(resource);
886
+ if (OAUTH_DEBUG_LOGS) {
887
+ logger.info({
888
+ event: "oauth_token_request_received",
889
+ req_id: res.locals["reqId"],
890
+ grant_type: req.body?.grant_type ?? null,
891
+ has_code: !!req.body?.code,
892
+ has_resource: !!req.body?.resource,
893
+ resource_host: resourceParts.host,
894
+ resource_path: resourceParts.path,
895
+ has_client_id: !!req.body?.client_id,
896
+ has_client_secret: !!req.body?.client_secret,
897
+ has_code_verifier: !!req.body?.code_verifier,
898
+ has_redirect_uri: !!req.body?.redirect_uri
899
+ });
900
+ }
901
+ if (req.body?.resource && !URL.canParse(req.body.resource)) {
902
+ delete req.body.resource;
903
+ }
904
+ const clientId2 = queryValue(req.body?.client_id);
905
+ const redirectUri = queryValue(req.body?.redirect_uri);
906
+ if (clientId2) {
907
+ provider.verifyAndRecover(clientId2, redirectUri && URL.canParse(redirectUri) ? redirectUri : void 0);
908
+ }
909
+ next();
910
+ });
911
+ if (OAUTH_DEBUG_LOGS) {
912
+ app.post("/register", express.json(), (req, res, next) => {
913
+ const redirectUris = arrayValue(req.body?.redirect_uris) ?? [];
914
+ logger.info({
915
+ event: "oauth_register_request_received",
916
+ req_id: res.locals["reqId"],
917
+ redirect_uri_count: redirectUris.length,
918
+ redirect_uri_hosts: [...new Set(redirectUris.map((uri) => urlParts2(uri).host).filter(Boolean))],
919
+ redirect_uri_paths: redirectUris.map((uri) => urlParts2(uri).path).filter(Boolean),
920
+ token_endpoint_auth_method: queryValue(req.body?.token_endpoint_auth_method) ?? null,
921
+ grant_types: arrayValue(req.body?.grant_types) ?? null,
922
+ response_types: arrayValue(req.body?.response_types) ?? null,
923
+ has_scope: typeof req.body?.scope === "string",
924
+ has_jwks_uri: typeof req.body?.jwks_uri === "string",
925
+ has_jwks: !!req.body?.jwks,
926
+ client_name: queryValue(req.body?.client_name) ?? null
927
+ });
928
+ next();
929
+ });
930
+ }
931
+ app.use("/authorize", express.urlencoded({ extended: false }), (req, _res, next) => {
932
+ const params = (req.method === "POST" ? req.body : req.query) ?? {};
933
+ const clientId2 = typeof params.client_id === "string" ? params.client_id : void 0;
934
+ const redirectUri = typeof params.redirect_uri === "string" ? params.redirect_uri : void 0;
935
+ const resource = typeof params.resource === "string" ? params.resource : void 0;
936
+ const redirectParts = urlParts2(redirectUri);
937
+ const resourceParts = urlParts2(resource);
938
+ if (OAUTH_DEBUG_LOGS) {
939
+ logger.info({
940
+ event: "oauth_authorize_request_received",
941
+ req_id: _res.locals["reqId"],
942
+ response_type: queryValue(params.response_type) ?? null,
943
+ client_id_len: clientId2?.length ?? 0,
944
+ redirect_uri_host: redirectParts.host,
945
+ redirect_uri_path: redirectParts.path,
946
+ resource_host: resourceParts.host,
947
+ resource_path: resourceParts.path,
948
+ has_state: typeof params.state === "string",
949
+ state_prefix: typeof params.state === "string" ? params.state.slice(0, 28) : null,
950
+ has_code_challenge: typeof params.code_challenge === "string",
951
+ code_challenge_method: queryValue(params.code_challenge_method) ?? null,
952
+ has_scope: typeof params.scope === "string"
953
+ });
954
+ }
955
+ if (clientId2 && redirectUri && URL.canParse(redirectUri)) {
956
+ provider.verifyAndRecover(clientId2, redirectUri);
957
+ }
958
+ next();
959
+ });
960
+ app.use(
961
+ mcpAuthRouter({
962
+ provider,
963
+ issuerUrl,
964
+ baseUrl: issuerUrl,
965
+ resourceServerUrl: new URL(mcpResourceUrl),
966
+ resourceName: "Plaud MCP Server"
967
+ })
968
+ );
969
+ app.post(
970
+ "/mcp",
971
+ requireBearerAuth({ verifier: provider, resourceMetadataUrl: protectedResourceMetadataUrl }),
972
+ async (req, res) => {
973
+ const token = req.auth.token;
974
+ const reqId = res.locals["reqId"] ?? randomUUID2();
975
+ const reqLog = logger.child({ req_id: reqId });
976
+ const startMs = Date.now();
977
+ reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
978
+ const client = new PlaudClient({
979
+ clientId,
980
+ clientSecret: "",
981
+ redirectUri: "",
982
+ apiBase,
983
+ staticToken: token
984
+ });
985
+ const mcpServer = new McpServer({ name: "plaud", version: "0.2.4" });
986
+ registerTools(mcpServer, client);
987
+ const transport = new StreamableHTTPServerTransport({
988
+ sessionIdGenerator: void 0,
989
+ // stateless
990
+ enableDnsRebindingProtection: true,
991
+ allowedOrigins: ALLOWED_ORIGINS
992
+ });
993
+ try {
994
+ await mcpServer.connect(transport);
995
+ await transport.handleRequest(req, res, req.body);
996
+ const clientVersion = mcpServer.server.getClientVersion();
997
+ if (clientVersion) {
998
+ reqLog.info({ event: "mcp_client_info", client_name: clientVersion.name, client_version: clientVersion.version });
999
+ }
1000
+ reqLog.info({ event: "mcp_request_end", duration_ms: Date.now() - startMs });
1001
+ } catch (err) {
1002
+ reqLog.error({ event: "mcp_request_error", error: String(err), duration_ms: Date.now() - startMs });
1003
+ if (!res.headersSent) {
1004
+ res.status(500).json({ error: String(err) });
1005
+ }
1006
+ } finally {
1007
+ await mcpServer.close();
1008
+ }
1009
+ }
1010
+ );
1011
+ if (!process.env.PLAUD_CALLBACK_URL) {
1012
+ const callbackApp = express();
1013
+ callbackApp.get(CALLBACK_PATH, (req, res) => {
1014
+ const code = req.query["code"];
1015
+ const state = req.query["state"];
1016
+ if (!code || !state) {
1017
+ logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
1018
+ res.status(400).send("Missing code or state");
1019
+ return;
1020
+ }
1021
+ provider.handleCallback(code, state, res);
1022
+ });
1023
+ createServer(callbackApp).listen(CALLBACK_PORT, "localhost", () => {
1024
+ logger.info({ event: "server_start", role: "oauth_callback", url: CALLBACK_URL });
1025
+ });
1026
+ }
1027
+ app.listen(HTTP_PORT, HTTP_HOST, () => {
1028
+ logger.info({
1029
+ event: "server_start",
1030
+ role: "mcp_http",
1031
+ url: serverUrl,
1032
+ mcp_endpoint: `${serverUrl}/mcp`,
1033
+ oauth_metadata: `${serverUrl}/.well-known/oauth-authorization-server`
1034
+ });
1035
+ });
1036
+ }
1037
+ export {
1038
+ startHttpServer
1039
+ };