@plaud-ai/mcp 0.1.32 → 0.2.0

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,370 @@
1
+ import {
2
+ getClient
3
+ } from "./chunk-7KGB7GSZ.js";
4
+ import {
5
+ copyToClipboard,
6
+ getMcpEntry,
7
+ writeSkillsToClaudeCode
8
+ } from "./chunk-UPEENHCG.js";
9
+ import {
10
+ skillsCombined
11
+ } from "./chunk-4QBEOJPX.js";
12
+ import "./chunk-SNSGVRCU.js";
13
+
14
+ // src/install.ts
15
+ import { readFile, writeFile, mkdir } from "fs/promises";
16
+ import { existsSync } from "fs";
17
+ import { createInterface } from "readline/promises";
18
+ import { createServer } from "http";
19
+ import { join, dirname } from "path";
20
+ import { homedir, platform } from "os";
21
+ import { spawnSync } from "child_process";
22
+ import open from "open";
23
+ function claudeDesktopConfigPath() {
24
+ if (platform() === "darwin") {
25
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
26
+ }
27
+ if (platform() === "win32") {
28
+ return join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
29
+ }
30
+ return null;
31
+ }
32
+ function claudeCodeDir() {
33
+ return join(homedir(), ".claude");
34
+ }
35
+ function codexConfigPath() {
36
+ return join(homedir(), ".codex", "config.toml");
37
+ }
38
+ function detectClients() {
39
+ const list = [];
40
+ const cdPath = claudeDesktopConfigPath();
41
+ list.push({
42
+ id: "claude-desktop",
43
+ label: "Claude Desktop",
44
+ detected: cdPath !== null && (existsSync(cdPath) || existsSync(dirname(cdPath))),
45
+ configPath: cdPath ?? "(unsupported on this OS)"
46
+ });
47
+ const ccDir = claudeCodeDir();
48
+ list.push({
49
+ id: "claude-code",
50
+ label: "Claude Code",
51
+ detected: existsSync(ccDir),
52
+ configPath: ccDir
53
+ });
54
+ const codexPath = codexConfigPath();
55
+ list.push({
56
+ id: "codex",
57
+ label: "Codex Desktop",
58
+ detected: existsSync(codexPath) || existsSync(dirname(codexPath)),
59
+ configPath: codexPath
60
+ });
61
+ return list;
62
+ }
63
+ async function prompt(question, defaultYes = true) {
64
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
65
+ const hint = defaultYes ? "Y/n" : "y/N";
66
+ const answer = (await rl.question(`${question} [${hint}] `)).trim().toLowerCase();
67
+ rl.close();
68
+ if (answer === "") return defaultYes;
69
+ return answer.startsWith("y");
70
+ }
71
+ async function installClaudeDesktop() {
72
+ const configPath = claudeDesktopConfigPath();
73
+ if (!configPath) return "skipped (OS not supported)";
74
+ let config = {};
75
+ try {
76
+ const raw = await readFile(configPath, "utf-8");
77
+ config = JSON.parse(raw);
78
+ } catch {
79
+ }
80
+ const mcpServers = config.mcpServers ?? {};
81
+ if (mcpServers.plaud) return "already configured";
82
+ config.mcpServers = { ...mcpServers, plaud: getMcpEntry() };
83
+ await mkdir(dirname(configPath), { recursive: true });
84
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
85
+ return "configured. Restart Claude Desktop to load the Plaud MCP.";
86
+ }
87
+ async function installClaudeCode() {
88
+ await writeSkillsToClaudeCode();
89
+ const { command, args } = getMcpEntry();
90
+ const manualCmd = `claude mcp add --scope user plaud ${command} ${args.join(" ")}`;
91
+ const cliCheck = spawnSync("which", ["claude"], { encoding: "utf-8" });
92
+ if (cliCheck.status !== 0) {
93
+ return `skills written to ~/.claude/CLAUDE.md. Claude Code CLI not on PATH \u2014 once installed, run:
94
+ ${manualCmd}`;
95
+ }
96
+ const existing = spawnSync("claude", ["mcp", "get", "plaud"], { encoding: "utf-8" });
97
+ if (existing.status === 0) {
98
+ return "skills written to ~/.claude/CLAUDE.md. MCP already registered (scope: user).";
99
+ }
100
+ const register = spawnSync(
101
+ "claude",
102
+ ["mcp", "add", "--scope", "user", "plaud", command, ...args],
103
+ { encoding: "utf-8" }
104
+ );
105
+ if (register.status !== 0) {
106
+ const err = (register.stderr || register.stdout || "").trim();
107
+ return `skills written to ~/.claude/CLAUDE.md. Failed to auto-register MCP (${err || "unknown"}). Run manually:
108
+ ${manualCmd}`;
109
+ }
110
+ return "skills written to ~/.claude/CLAUDE.md and MCP registered at user scope.";
111
+ }
112
+ async function installCodex() {
113
+ const configPath = codexConfigPath();
114
+ const { command, args } = getMcpEntry();
115
+ const argsStr = args.map((a) => `"${a}"`).join(", ");
116
+ const entry = `
117
+ [mcp_servers.plaud]
118
+ command = "${command}"
119
+ args = [${argsStr}]
120
+ `;
121
+ let content = "";
122
+ try {
123
+ content = await readFile(configPath, "utf-8");
124
+ } catch {
125
+ }
126
+ if (content.includes("[mcp_servers.plaud]")) return "already configured";
127
+ await mkdir(dirname(configPath), { recursive: true });
128
+ await writeFile(configPath, content + entry, "utf-8");
129
+ await writeSkillsToClaudeCode();
130
+ const combined = await skillsCombined();
131
+ const copied = copyToClipboard(combined);
132
+ return copied ? "configured. Skills copied to clipboard \u2014 paste into Codex custom instructions, then restart." : "configured. Paste the Plaud skills into Codex custom instructions manually (see docs).";
133
+ }
134
+ var CALLBACK_PORT = 8199;
135
+ var LOGIN_TIMEOUT_MS = 12e4;
136
+ function pickIdentity(user) {
137
+ for (const k of ["email", "username", "name", "id"]) {
138
+ const v = user[k];
139
+ if (typeof v === "string" && v.length > 0) return v;
140
+ }
141
+ return void 0;
142
+ }
143
+ async function runLogin() {
144
+ const client = getClient();
145
+ try {
146
+ const existing = await client.auth.getAccessToken();
147
+ if (existing) {
148
+ try {
149
+ const user = await client.getCurrentUser();
150
+ return { status: "already-authed", who: pickIdentity(user) };
151
+ } catch {
152
+ return { status: "already-authed" };
153
+ }
154
+ }
155
+ } catch {
156
+ await client.auth.logout().catch(() => void 0);
157
+ }
158
+ const { url, codeVerifier, state } = client.auth.createAuthorizationRequest();
159
+ return new Promise((resolve) => {
160
+ const httpServer = createServer(async (req, res) => {
161
+ const reqUrl = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
162
+ if (reqUrl.pathname !== "/auth/callback") {
163
+ res.writeHead(404);
164
+ res.end();
165
+ return;
166
+ }
167
+ const code = reqUrl.searchParams.get("code");
168
+ if (!code) {
169
+ res.writeHead(400);
170
+ res.end("Missing code");
171
+ cleanup();
172
+ resolve({ status: "failed", message: "missing authorization code in callback" });
173
+ return;
174
+ }
175
+ try {
176
+ await client.auth.exchangeCode(code, codeVerifier, state);
177
+ res.writeHead(200, { "Content-Type": "text/html" });
178
+ res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
179
+ cleanup();
180
+ let who;
181
+ try {
182
+ const user = await client.getCurrentUser();
183
+ who = pickIdentity(user);
184
+ } catch {
185
+ }
186
+ resolve({ status: "success", who });
187
+ } catch (err) {
188
+ res.writeHead(500, { "Content-Type": "text/html" });
189
+ res.end("<h1>Token exchange failed</h1>");
190
+ cleanup();
191
+ resolve({ status: "failed", message: err instanceof Error ? err.message : String(err) });
192
+ }
193
+ });
194
+ let timeoutId;
195
+ function cleanup() {
196
+ clearTimeout(timeoutId);
197
+ httpServer.closeAllConnections?.();
198
+ httpServer.close();
199
+ }
200
+ timeoutId = setTimeout(() => {
201
+ cleanup();
202
+ resolve({ status: "timeout" });
203
+ }, LOGIN_TIMEOUT_MS);
204
+ httpServer.listen(CALLBACK_PORT, () => {
205
+ open(url).catch(() => {
206
+ cleanup();
207
+ resolve({ status: "failed", message: `could not open browser. Visit manually: ${url}` });
208
+ });
209
+ });
210
+ httpServer.on("error", (err) => {
211
+ cleanup();
212
+ resolve({ status: "failed", message: `callback server error: ${err.message}` });
213
+ });
214
+ });
215
+ }
216
+ async function runInstall(opts = {}) {
217
+ console.log("Plaud MCP installer\n");
218
+ const clients = detectClients();
219
+ console.log("Detected AI clients:");
220
+ for (const c of clients) {
221
+ const mark = c.detected ? "\u2713" : "\xB7";
222
+ console.log(` ${mark} ${c.label.padEnd(16)} ${c.detected ? c.configPath : "(not detected)"}`);
223
+ }
224
+ console.log();
225
+ const selected = [];
226
+ if (opts.yes) {
227
+ for (const c of clients) {
228
+ if (c.detected) selected.push(c);
229
+ }
230
+ if (selected.length > 0) {
231
+ console.log(`--yes: configuring ${selected.map((c) => c.label).join(", ")} without prompting.
232
+ `);
233
+ }
234
+ } else {
235
+ for (const c of clients) {
236
+ if (!c.detected) continue;
237
+ const yes = await prompt(`Configure ${c.label}?`, true);
238
+ if (yes) selected.push(c);
239
+ }
240
+ }
241
+ if (selected.length === 0) {
242
+ console.log("\nNothing to configure. Exiting.");
243
+ process.exit(0);
244
+ }
245
+ console.log();
246
+ const succeeded = [];
247
+ for (const c of selected) {
248
+ process.stdout.write(`\u2192 ${c.label}... `);
249
+ try {
250
+ let result = "";
251
+ if (c.id === "claude-desktop") result = await installClaudeDesktop();
252
+ else if (c.id === "claude-code") result = await installClaudeCode();
253
+ else if (c.id === "codex") result = await installCodex();
254
+ console.log(result);
255
+ if (!/^(skipped|failed)/i.test(result)) succeeded.push(c);
256
+ } catch (err) {
257
+ console.log(`failed: ${err instanceof Error ? err.message : String(err)}`);
258
+ }
259
+ }
260
+ const loginOutcome = opts.noLogin ? null : await doLoginStep(Boolean(opts.yes));
261
+ printNextSteps(succeeded, loginOutcome);
262
+ process.exit(0);
263
+ }
264
+ async function doLoginStep(nonInteractive) {
265
+ console.log();
266
+ console.log("\u2192 Authenticating with Plaud...");
267
+ const client = getClient();
268
+ try {
269
+ const existing = await client.auth.getAccessToken();
270
+ if (existing) {
271
+ try {
272
+ const user = await client.getCurrentUser();
273
+ const who = pickIdentity(user);
274
+ console.log(` already signed in${who ? ` as ${who}` : ""} \u2014 skipping OAuth.`);
275
+ return { status: "already-authed", who };
276
+ } catch {
277
+ console.log(" token present but user lookup failed \u2014 will re-auth.");
278
+ }
279
+ }
280
+ } catch {
281
+ }
282
+ if (!nonInteractive) {
283
+ const yes = await prompt("Log in to Plaud now? (opens browser)", true);
284
+ if (!yes) {
285
+ console.log(" skipped \u2014 you'll be prompted on first tool call after restart.");
286
+ return null;
287
+ }
288
+ }
289
+ console.log(" opening browser \u2014 click Authorize to finish.");
290
+ const outcome = await runLogin();
291
+ switch (outcome.status) {
292
+ case "already-authed":
293
+ console.log(` already signed in${outcome.who ? ` as ${outcome.who}` : ""}.`);
294
+ break;
295
+ case "success":
296
+ console.log(` \u2713 authenticated${outcome.who ? ` as ${outcome.who}` : ""}.`);
297
+ break;
298
+ case "timeout":
299
+ console.log(" \u2717 timed out after 2 minutes \u2014 you can retry with `plaud-mcp install --yes` or run `login` from your AI client.");
300
+ break;
301
+ case "failed":
302
+ console.log(` \u2717 failed: ${outcome.message}`);
303
+ break;
304
+ }
305
+ return outcome;
306
+ }
307
+ function restartLine(id) {
308
+ switch (id) {
309
+ case "claude-desktop":
310
+ return "Fully quit Claude Desktop (\u2318Q on macOS) and reopen it \u2014 closing the window is not enough.";
311
+ case "claude-code":
312
+ return "Exit any running Claude Code session and start a new `claude` session.";
313
+ case "codex":
314
+ return "Quit Codex Desktop and reopen it.";
315
+ }
316
+ }
317
+ function printNextSteps(clients, login) {
318
+ console.log();
319
+ if (clients.length === 0) {
320
+ console.log("No clients were configured. Nothing to do.");
321
+ return;
322
+ }
323
+ const authed = login?.status === "already-authed" || login?.status === "success";
324
+ const bar = "\u2500".repeat(60);
325
+ console.log(bar);
326
+ console.log(`\u2713 Plaud MCP installed for: ${clients.map((c) => c.label).join(", ")}`);
327
+ if (authed) {
328
+ console.log(`\u2713 Authenticated${login?.who ? ` as ${login.who}` : ""}`);
329
+ }
330
+ console.log(bar);
331
+ console.log();
332
+ if (authed) {
333
+ console.log("NEXT \u2014 one thing left:");
334
+ console.log();
335
+ if (clients.length === 1) {
336
+ console.log(` ${restartLine(clients[0].id)}`);
337
+ } else {
338
+ console.log(" Restart each configured client:");
339
+ for (const c of clients) {
340
+ console.log(` \u2022 ${c.label}: ${restartLine(c.id)}`);
341
+ }
342
+ }
343
+ console.log();
344
+ console.log(" After restart, Plaud tools are live \u2014 just ask your AI client");
345
+ console.log(' about your recordings (e.g. "list my recent Plaud recordings").');
346
+ } else {
347
+ console.log("NEXT \u2014 do these two things:");
348
+ console.log();
349
+ if (clients.length === 1) {
350
+ console.log(` 1. ${restartLine(clients[0].id)}`);
351
+ } else {
352
+ console.log(" 1. Restart each configured client:");
353
+ for (const c of clients) {
354
+ console.log(` \u2022 ${c.label}: ${restartLine(c.id)}`);
355
+ }
356
+ }
357
+ console.log();
358
+ console.log(" 2. In any chat with your AI client, type:");
359
+ console.log();
360
+ console.log(" list my recent Plaud recordings");
361
+ console.log();
362
+ console.log(" This triggers OAuth in your browser (once). Tokens are then");
363
+ console.log(" stored in ~/.plaud/tokens.json and refreshed automatically.");
364
+ }
365
+ console.log();
366
+ console.log(bar);
367
+ }
368
+ export {
369
+ runInstall
370
+ };
@@ -0,0 +1,294 @@
1
+ import {
2
+ logger,
3
+ registerTools
4
+ } from "./chunk-MPCF6HMK.js";
5
+ import {
6
+ PlaudClient
7
+ } from "./chunk-SNSGVRCU.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 { 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 { randomUUID } from "crypto";
21
+ import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
22
+ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
23
+ _plaudClientId;
24
+ _plaudClientSecret;
25
+ _plaudTokenUrl;
26
+ _plaudApiBase;
27
+ _callbackUrl;
28
+ _registeredClients = /* @__PURE__ */ new Map();
29
+ // internalState → { clientRedirectUri, originalState }
30
+ // We generate our own state to track the pending flow regardless of whether the client sent one.
31
+ _pendingStates = /* @__PURE__ */ new Map();
32
+ // code → internalState: lets exchangeAuthorizationCode include state in the Plaud token request
33
+ _pendingCodes = /* @__PURE__ */ new Map();
34
+ constructor(options) {
35
+ const authUrl = options.authUrl ?? "https://web.plaud.ai/platform/oauth";
36
+ const tokenUrl = options.tokenUrl ?? "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
37
+ const apiBase = options.apiBase ?? "https://platform.plaud.ai/developer/api";
38
+ super({
39
+ endpoints: {
40
+ authorizationUrl: authUrl,
41
+ tokenUrl
42
+ },
43
+ verifyAccessToken: async (token) => {
44
+ const client = new PlaudClient({
45
+ clientId: options.clientId,
46
+ clientSecret: options.clientSecret,
47
+ redirectUri: "",
48
+ apiBase,
49
+ staticToken: token
50
+ });
51
+ try {
52
+ const user = await client.getCurrentUser();
53
+ let expiresAt;
54
+ try {
55
+ const payload = JSON.parse(
56
+ Buffer.from(token.split(".")[1], "base64url").toString()
57
+ );
58
+ expiresAt = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + 3600;
59
+ } catch {
60
+ expiresAt = Math.floor(Date.now() / 1e3) + 3600;
61
+ }
62
+ const authInfo = {
63
+ token,
64
+ clientId: String(user.id ?? "unknown"),
65
+ scopes: [],
66
+ expiresAt
67
+ };
68
+ logger.info({ event: "token_verified", client_id: authInfo.clientId, expires_at: expiresAt });
69
+ return authInfo;
70
+ } catch (err) {
71
+ logger.warn({ event: "token_verify_failed", error: String(err) });
72
+ throw new Error("Invalid or expired token");
73
+ }
74
+ },
75
+ getClient: async (id) => this._registeredClients.get(id)
76
+ });
77
+ this._plaudClientId = options.clientId;
78
+ this._plaudClientSecret = options.clientSecret;
79
+ this._plaudTokenUrl = tokenUrl;
80
+ this._plaudApiBase = apiBase;
81
+ this._callbackUrl = options.callbackUrl;
82
+ this.skipLocalPkceValidation = true;
83
+ }
84
+ // Override clientsStore to add in-memory dynamic client registration
85
+ get clientsStore() {
86
+ return {
87
+ getClient: async (id) => this._registeredClients.get(id),
88
+ registerClient: async (client) => {
89
+ const full = {
90
+ ...client,
91
+ client_id: randomUUID(),
92
+ client_id_issued_at: Math.floor(Date.now() / 1e3)
93
+ };
94
+ this._registeredClients.set(full.client_id, full);
95
+ return full;
96
+ }
97
+ };
98
+ }
99
+ /**
100
+ * Redirect to Plaud using our own registered callback URL.
101
+ * Store the client's original redirect_uri keyed by state so we can forward after Plaud calls back.
102
+ */
103
+ async authorize(_client, params, res) {
104
+ const internalState = randomUUID();
105
+ this._pendingStates.set(internalState, {
106
+ clientRedirectUri: params.redirectUri,
107
+ originalState: params.state
108
+ });
109
+ logger.info({ event: "oauth_authorize_start", internal_state: internalState, redirect_uri: params.redirectUri });
110
+ const targetUrl = new URL(this._endpoints.authorizationUrl);
111
+ const searchParams = new URLSearchParams({
112
+ client_id: this._plaudClientId,
113
+ response_type: "code",
114
+ redirect_uri: this._callbackUrl,
115
+ code_challenge: params.codeChallenge,
116
+ code_challenge_method: "S256",
117
+ state: internalState
118
+ // always send our internal state to Plaud
119
+ });
120
+ if (params.scopes?.length) searchParams.set("scope", params.scopes.join(" "));
121
+ targetUrl.search = searchParams.toString();
122
+ res.redirect(targetUrl.toString());
123
+ }
124
+ /**
125
+ * Called when Plaud redirects to our /oauth/callback.
126
+ * Looks up the original client redirect_uri and forwards the code+state to it.
127
+ */
128
+ handleCallback(code, state, res) {
129
+ const pending = this._pendingStates.get(state);
130
+ if (!pending) {
131
+ logger.warn({ event: "oauth_callback_unknown_state", state });
132
+ res.status(400).send("Unknown state \u2014 authorization request not found");
133
+ return;
134
+ }
135
+ this._pendingStates.delete(state);
136
+ this._pendingCodes.set(code, state);
137
+ logger.info({ event: "oauth_callback_received", internal_state: state });
138
+ const target = new URL(pending.clientRedirectUri);
139
+ target.searchParams.set("code", code);
140
+ if (pending.originalState) {
141
+ target.searchParams.set("state", pending.originalState);
142
+ }
143
+ res.redirect(target.toString());
144
+ }
145
+ // Override to use Plaud's Basic auth + our fixed callback URL for redirect_uri
146
+ async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, _redirectUri) {
147
+ const basicAuth = Buffer.from(
148
+ `${this._plaudClientId}:${this._plaudClientSecret}`
149
+ ).toString("base64");
150
+ const internalState = this._pendingCodes.get(authorizationCode);
151
+ this._pendingCodes.delete(authorizationCode);
152
+ const body = {
153
+ grant_type: "authorization_code",
154
+ code: authorizationCode,
155
+ redirect_uri: this._callbackUrl
156
+ };
157
+ if (codeVerifier) body.code_verifier = codeVerifier;
158
+ if (internalState) body.state = internalState;
159
+ const fetchRes = await fetch(this._plaudTokenUrl, {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/x-www-form-urlencoded",
163
+ Accept: "application/json",
164
+ Authorization: `Basic ${basicAuth}`
165
+ },
166
+ body: new URLSearchParams(body)
167
+ });
168
+ if (!fetchRes.ok) {
169
+ const text = await fetchRes.text();
170
+ logger.error({ event: "oauth_token_exchange_failed", status: fetchRes.status, body: text });
171
+ throw new Error(`Token exchange failed: ${fetchRes.status} ${text}`);
172
+ }
173
+ const data = await fetchRes.json();
174
+ logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
175
+ return {
176
+ access_token: data.access_token,
177
+ token_type: data.token_type ?? "Bearer",
178
+ refresh_token: data.refresh_token,
179
+ expires_in: data.expires_in
180
+ };
181
+ }
182
+ };
183
+
184
+ // src/http/server.ts
185
+ var HTTP_PORT = Number(process.env.PLAUD_HTTP_PORT ?? 3e3);
186
+ var HTTP_HOST = process.env.PLAUD_HTTP_HOST ?? "0.0.0.0";
187
+ var CALLBACK_PORT = 8199;
188
+ var CALLBACK_PATH = "/auth/callback";
189
+ var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
190
+ function startHttpServer() {
191
+ const clientId = process.env.PLAUD_MCP_CLIENT_ID ?? process.env.PLAUD_CLIENT_ID ?? "client_9c501dad-8a0d-40b2-a7b0-d1cb8787f674";
192
+ const clientSecret = process.env.PLAUD_CLIENT_SECRET ?? "";
193
+ const apiBase = process.env.PLAUD_API_BASE;
194
+ const serverUrl = process.env.PLAUD_SERVER_URL ?? `http://localhost:${HTTP_PORT}`;
195
+ const provider = new PlaudOAuthProvider({
196
+ clientId,
197
+ clientSecret,
198
+ callbackUrl: CALLBACK_URL,
199
+ authUrl: process.env.PLAUD_AUTH_URL,
200
+ tokenUrl: process.env.PLAUD_TOKEN_URL,
201
+ apiBase
202
+ });
203
+ const issuerUrl = new URL(serverUrl);
204
+ const app = createMcpExpressApp({ host: HTTP_HOST });
205
+ app.get("/health", (_req, res) => {
206
+ res.json({ status: "ok", uptime_s: Math.floor(process.uptime()) });
207
+ });
208
+ app.use((req, res, next) => {
209
+ const reqId = req.headers["x-request-id"] ?? randomUUID2();
210
+ res.locals["reqId"] = reqId;
211
+ logger.info({ event: "http_request", req_id: reqId, method: req.method, path: req.url, user_agent: req.headers["user-agent"] ?? null });
212
+ next();
213
+ });
214
+ app.post("/token", express.urlencoded({ extended: false }), (req, _res, next) => {
215
+ if (req.body?.resource && !URL.canParse(req.body.resource)) {
216
+ delete req.body.resource;
217
+ }
218
+ next();
219
+ });
220
+ app.use(
221
+ mcpAuthRouter({
222
+ provider,
223
+ issuerUrl,
224
+ baseUrl: issuerUrl,
225
+ resourceName: "Plaud MCP Server"
226
+ })
227
+ );
228
+ app.post(
229
+ "/mcp",
230
+ requireBearerAuth({ verifier: provider, resourceMetadataUrl: `${serverUrl}/.well-known/oauth-protected-resource` }),
231
+ async (req, res) => {
232
+ const token = req.auth.token;
233
+ const reqId = res.locals["reqId"] ?? randomUUID2();
234
+ const reqLog = logger.child({ req_id: reqId });
235
+ const startMs = Date.now();
236
+ reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
237
+ const client = new PlaudClient({
238
+ clientId,
239
+ clientSecret,
240
+ redirectUri: "",
241
+ apiBase,
242
+ staticToken: token
243
+ });
244
+ const mcpServer = new McpServer({ name: "plaud", version: "0.1.0" });
245
+ registerTools(mcpServer, client);
246
+ const transport = new StreamableHTTPServerTransport({
247
+ sessionIdGenerator: void 0
248
+ // stateless
249
+ });
250
+ try {
251
+ await mcpServer.connect(transport);
252
+ await transport.handleRequest(req, res, req.body);
253
+ const clientVersion = mcpServer.server.getClientVersion();
254
+ if (clientVersion) {
255
+ reqLog.info({ event: "mcp_client_info", client_name: clientVersion.name, client_version: clientVersion.version });
256
+ }
257
+ reqLog.info({ event: "mcp_request_end", duration_ms: Date.now() - startMs });
258
+ } catch (err) {
259
+ reqLog.error({ event: "mcp_request_error", error: String(err), duration_ms: Date.now() - startMs });
260
+ if (!res.headersSent) {
261
+ res.status(500).json({ error: String(err) });
262
+ }
263
+ } finally {
264
+ await mcpServer.close();
265
+ }
266
+ }
267
+ );
268
+ const callbackApp = express();
269
+ callbackApp.get(CALLBACK_PATH, (req, res) => {
270
+ const code = req.query["code"];
271
+ const state = req.query["state"];
272
+ if (!code || !state) {
273
+ logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
274
+ res.status(400).send("Missing code or state");
275
+ return;
276
+ }
277
+ provider.handleCallback(code, state, res);
278
+ });
279
+ createServer(callbackApp).listen(CALLBACK_PORT, "localhost", () => {
280
+ logger.info({ event: "server_start", role: "oauth_callback", url: CALLBACK_URL });
281
+ });
282
+ app.listen(HTTP_PORT, HTTP_HOST, () => {
283
+ logger.info({
284
+ event: "server_start",
285
+ role: "mcp_http",
286
+ url: serverUrl,
287
+ mcp_endpoint: `${serverUrl}/mcp`,
288
+ oauth_metadata: `${serverUrl}/.well-known/oauth-authorization-server`
289
+ });
290
+ });
291
+ }
292
+ export {
293
+ startHttpServer
294
+ };