@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.
@@ -229,6 +229,124 @@ var PlaudClient = class {
229
229
  }
230
230
  };
231
231
 
232
+ // ../shared/dist/oauth-callback-server.js
233
+ import { createServer } from "http";
234
+ var SUCCESS_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization successful!</h1><p>You can close this tab.</p></body></html>';
235
+ var NEUTRAL_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Continue authorization in the original window.</h1><p>This page can be closed.</p></body></html>';
236
+ function errorHtml(message) {
237
+ const escaped = message.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
238
+ return '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization failed</h1><pre style="white-space:pre-wrap;">' + escaped + "</pre></body></html>";
239
+ }
240
+ var CORS_HEADERS = {
241
+ "Access-Control-Allow-Origin": "*",
242
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
243
+ "Access-Control-Allow-Headers": "*"
244
+ };
245
+ function runOAuthCallback(opts) {
246
+ const { port, expectedState, exchangeCode, timeoutMs = 12e4, onListening, postSuccessDelayMs = 1500 } = opts;
247
+ return new Promise((resolve) => {
248
+ let settled = false;
249
+ let exchangeStarted = false;
250
+ let exchangeSucceeded = false;
251
+ let timeoutId = null;
252
+ let closeTimeoutId = null;
253
+ const server = createServer((req, res) => {
254
+ if (req.method === "OPTIONS") {
255
+ res.writeHead(204, CORS_HEADERS);
256
+ res.end();
257
+ return;
258
+ }
259
+ const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
260
+ if (reqUrl.pathname !== "/auth/callback") {
261
+ res.writeHead(404, CORS_HEADERS);
262
+ res.end();
263
+ return;
264
+ }
265
+ const params = reqUrl.searchParams;
266
+ const error = params.get("error");
267
+ const state = params.get("state");
268
+ const code = params.get("code");
269
+ if (error) {
270
+ const desc = params.get("error_description") ?? error;
271
+ res.writeHead(400, { "Content-Type": "text/html", ...CORS_HEADERS });
272
+ res.end(errorHtml(`Authorization denied: ${desc}`));
273
+ finalize({ status: "denied", error: new Error(desc) });
274
+ return;
275
+ }
276
+ if (!state || state !== expectedState) {
277
+ respondNeutral(res);
278
+ return;
279
+ }
280
+ if (exchangeSucceeded) {
281
+ respondSuccess(res);
282
+ return;
283
+ }
284
+ if (!code) {
285
+ respondNeutral(res);
286
+ return;
287
+ }
288
+ if (exchangeStarted) {
289
+ respondNeutral(res);
290
+ return;
291
+ }
292
+ exchangeStarted = true;
293
+ exchangeCode(code).then(() => {
294
+ exchangeSucceeded = true;
295
+ respondSuccess(res);
296
+ finalize({ status: "success" });
297
+ }, (err) => {
298
+ const e = err instanceof Error ? err : new Error(String(err));
299
+ res.writeHead(500, { "Content-Type": "text/html", ...CORS_HEADERS });
300
+ res.end(errorHtml(e.message));
301
+ finalize({ status: "exchange-failed", error: e });
302
+ });
303
+ });
304
+ server.on("error", (err) => {
305
+ if (settled)
306
+ return;
307
+ const message = err.code === "EADDRINUSE" ? `port ${port} is in use \u2014 another \`plaud login\` may still be running. Wait a few seconds and retry.` : `callback server error: ${err.message}`;
308
+ finalize({ status: "listen-failed", error: new Error(message) }, true);
309
+ });
310
+ timeoutId = setTimeout(() => {
311
+ finalize({ status: "timeout" }, true);
312
+ }, timeoutMs);
313
+ server.listen(port, () => {
314
+ onListening?.();
315
+ });
316
+ function respondSuccess(res) {
317
+ res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
318
+ res.end(SUCCESS_HTML);
319
+ }
320
+ function respondNeutral(res) {
321
+ res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
322
+ res.end(NEUTRAL_HTML);
323
+ }
324
+ function finalize(result, immediate = false) {
325
+ if (settled)
326
+ return;
327
+ settled = true;
328
+ if (timeoutId) {
329
+ clearTimeout(timeoutId);
330
+ timeoutId = null;
331
+ }
332
+ const close = () => {
333
+ try {
334
+ server.closeAllConnections?.();
335
+ } catch {
336
+ }
337
+ server.close(() => resolve(result));
338
+ };
339
+ if (immediate || result.status !== "success") {
340
+ close();
341
+ } else {
342
+ closeTimeoutId = setTimeout(close, postSuccessDelayMs);
343
+ closeTimeoutId.unref?.();
344
+ }
345
+ }
346
+ });
347
+ }
348
+
232
349
  export {
233
- PlaudClient
350
+ PlaudClient,
351
+ runOAuthCallback
234
352
  };
@@ -20,6 +20,12 @@ function registerTools(server, client) {
20
20
  "list_files",
21
21
  {
22
22
  description: "List Plaud recordings. Supports optional client-side filtering: `query` (case-insensitive name substring), `date_from`/`date_to` (YYYY-MM-DD, inclusive). When any filter is set, paginates up to 5 pages \xD7 100 recordings and returns all matches.",
23
+ annotations: {
24
+ title: "List recordings",
25
+ readOnlyHint: true,
26
+ destructiveHint: false,
27
+ openWorldHint: true
28
+ },
23
29
  inputSchema: {
24
30
  page: z.number().optional().default(1).describe("Page number (ignored when filters are set)"),
25
31
  page_size: z.number().optional().default(20).describe("Items per page (ignored when filters are set)"),
@@ -82,6 +88,12 @@ function registerTools(server, client) {
82
88
  "get_file",
83
89
  {
84
90
  description: "Get details of a specific Plaud recording by ID",
91
+ annotations: {
92
+ title: "Get recording details",
93
+ readOnlyHint: true,
94
+ destructiveHint: false,
95
+ openWorldHint: true
96
+ },
85
97
  inputSchema: { file_id: z.string().describe("The file ID to retrieve") }
86
98
  },
87
99
  async ({ file_id }) => {
@@ -101,6 +113,12 @@ function registerTools(server, client) {
101
113
  "get_note",
102
114
  {
103
115
  description: "Fetch AI-generated notes for a Plaud recording \u2014 compact summary, action items, and key topics",
116
+ annotations: {
117
+ title: "Get recording notes",
118
+ readOnlyHint: true,
119
+ destructiveHint: false,
120
+ openWorldHint: true
121
+ },
104
122
  inputSchema: { file_id: z.string().describe("The file ID to retrieve notes for") }
105
123
  },
106
124
  async ({ file_id }) => {
@@ -120,6 +138,12 @@ function registerTools(server, client) {
120
138
  "get_transcript",
121
139
  {
122
140
  description: "Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
141
+ annotations: {
142
+ title: "Get recording transcript",
143
+ readOnlyHint: true,
144
+ destructiveHint: false,
145
+ openWorldHint: true
146
+ },
123
147
  inputSchema: { file_id: z.string().describe("The file ID to retrieve transcript for") }
124
148
  },
125
149
  async ({ file_id }) => {
@@ -138,7 +162,13 @@ function registerTools(server, client) {
138
162
  server.registerTool(
139
163
  "get_current_user",
140
164
  {
141
- description: "Get current authenticated user info"
165
+ description: "Get current authenticated user info",
166
+ annotations: {
167
+ title: "Get current user",
168
+ readOnlyHint: true,
169
+ destructiveHint: false,
170
+ openWorldHint: true
171
+ }
142
172
  },
143
173
  async () => {
144
174
  const start = Date.now();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PlaudClient
3
- } from "./chunk-SNSGVRCU.js";
3
+ } from "./chunk-3XRFIJUG.js";
4
4
 
5
5
  // src/config.ts
6
6
  function buildExtraHeaders() {
package/dist/index.js CHANGED
@@ -1,23 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient
4
- } from "./chunk-7KGB7GSZ.js";
4
+ } from "./chunk-GZ7QPRKV.js";
5
5
  import {
6
6
  loadSkills
7
7
  } from "./chunk-4QBEOJPX.js";
8
8
  import {
9
9
  registerTools
10
- } from "./chunk-IZKXHQM3.js";
11
- import "./chunk-SNSGVRCU.js";
10
+ } from "./chunk-BZOIX6WC.js";
11
+ import {
12
+ runOAuthCallback
13
+ } from "./chunk-3XRFIJUG.js";
12
14
 
13
15
  // src/index.ts
14
- import { createServer } from "http";
15
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
18
  import open from "open";
18
19
  var server = new McpServer({
19
20
  name: "plaud",
20
- version: "0.2.2"
21
+ version: "0.2.4"
21
22
  });
22
23
  var CALLBACK_PORT = 8199;
23
24
  var LOGIN_TIMEOUT_MS = 12e4;
@@ -27,82 +28,64 @@ server.registerTool("login", {
27
28
  const client = getClient();
28
29
  const existingToken = await client.auth.getAccessToken();
29
30
  if (existingToken) {
30
- return { content: [{ type: "text", text: "Already logged in." }] };
31
- }
32
- const { url, codeVerifier, state } = client.auth.createAuthorizationRequest();
33
- return new Promise((resolve) => {
34
- const httpServer = createServer(async (req, res) => {
35
- const reqUrl = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
36
- if (reqUrl.pathname !== "/auth/callback") {
37
- res.writeHead(404);
38
- res.end();
39
- return;
40
- }
41
- const code = reqUrl.searchParams.get("code");
42
- if (!code) {
43
- res.writeHead(400);
44
- res.end("Missing code");
45
- cleanup();
46
- resolve({
47
- content: [{ type: "text", text: "Authentication failed: missing authorization code in callback" }],
48
- isError: true
49
- });
50
- return;
51
- }
52
- try {
53
- await client.auth.exchangeCode(code, codeVerifier, state);
54
- res.writeHead(200, { "Content-Type": "text/html" });
55
- res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
56
- cleanup();
57
- resolve({
58
- content: [{ type: "text", text: "Successfully authenticated with Plaud!" }]
59
- });
60
- } catch (err) {
61
- const errorMsg = err instanceof Error ? err.message : String(err);
62
- res.writeHead(500, { "Content-Type": "text/html" });
63
- res.end(`<h1>Token exchange failed</h1><pre>${errorMsg}</pre>`);
64
- cleanup();
65
- resolve({
66
- content: [{ type: "text", text: `Authentication failed: ${err}` }],
31
+ try {
32
+ await client.getCurrentUser();
33
+ return { content: [{ type: "text", text: "Already logged in." }] };
34
+ } catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ if (msg.includes("401") || msg.includes("Not authenticated")) {
37
+ await client.auth.logout();
38
+ } else {
39
+ return {
40
+ content: [{ type: "text", text: `Failed to verify login state: ${msg}` }],
67
41
  isError: true
68
- });
42
+ };
69
43
  }
70
- });
71
- let timeoutId;
72
- function cleanup() {
73
- clearTimeout(timeoutId);
74
- httpServer.closeAllConnections?.();
75
- httpServer.close();
76
44
  }
77
- timeoutId = setTimeout(() => {
78
- cleanup();
79
- resolve({
80
- content: [{ type: "text", text: "Authentication timed out after 2 minutes. Please try again." }],
81
- isError: true
82
- });
83
- }, LOGIN_TIMEOUT_MS);
84
- httpServer.listen(CALLBACK_PORT, () => {
45
+ }
46
+ const { url, codeVerifier, state } = client.auth.createAuthorizationRequest();
47
+ const result = await runOAuthCallback({
48
+ port: CALLBACK_PORT,
49
+ expectedState: state,
50
+ timeoutMs: LOGIN_TIMEOUT_MS,
51
+ exchangeCode: async (code) => {
52
+ await client.auth.exchangeCode(code, codeVerifier, state);
53
+ },
54
+ onListening: () => {
85
55
  open(url).catch(() => {
86
- resolve({
87
- content: [{
88
- type: "text",
89
- text: `Could not open browser automatically. Open this URL within 2 minutes to authenticate:
56
+ });
57
+ }
58
+ });
59
+ switch (result.status) {
60
+ case "success":
61
+ return { content: [{ type: "text", text: "Successfully authenticated with Plaud!" }] };
62
+ case "timeout":
63
+ return {
64
+ content: [{
65
+ type: "text",
66
+ text: `Authentication timed out after 2 minutes. If no browser opened, open this URL on the same machine and retry:
90
67
  ${url}
91
68
 
92
- If this is a remote/headless machine, forward local port 8199 to this machine first, then open the URL locally.`
93
- }],
94
- isError: true
95
- });
96
- });
97
- });
98
- httpServer.on("error", (err) => {
99
- cleanup();
100
- resolve({
101
- content: [{ type: "text", text: `Failed to start callback server: ${err.message}` }],
69
+ If this is a remote/headless machine, forward local port ${CALLBACK_PORT} first.`
70
+ }],
102
71
  isError: true
103
- });
104
- });
105
- });
72
+ };
73
+ case "denied":
74
+ return {
75
+ content: [{ type: "text", text: `Authentication denied: ${result.error?.message ?? "user declined authorization"}` }],
76
+ isError: true
77
+ };
78
+ case "exchange-failed":
79
+ return {
80
+ content: [{ type: "text", text: `Authentication failed: ${result.error?.message ?? "token exchange failed"}` }],
81
+ isError: true
82
+ };
83
+ case "listen-failed":
84
+ return {
85
+ content: [{ type: "text", text: `Failed to start callback server: ${result.error?.message ?? "unknown error"}` }],
86
+ isError: true
87
+ };
88
+ }
106
89
  });
107
90
  registerTools(server, getClient());
108
91
  server.registerTool("logout", {
@@ -134,7 +117,7 @@ async function main() {
134
117
  const sub = process.argv[2];
135
118
  const sub2 = process.argv[3];
136
119
  if (sub === "install") {
137
- const { runInstall } = await import("./install-EEQXUUG3.js");
120
+ const { runInstall } = await import("./install-ALLWR7CL.js");
138
121
  const args = process.argv.slice(3);
139
122
  const yes = args.some((a) => a === "--yes" || a === "-y");
140
123
  const noLogin = args.some((a) => a === "--no-login");
@@ -167,7 +150,7 @@ async function main() {
167
150
  return;
168
151
  }
169
152
  if (sub === "http") {
170
- const { startHttpServer } = await import("./server-QWAZPBHU.js");
153
+ const { startHttpServer } = await import("./server-VY7ANVCH.js");
171
154
  startHttpServer();
172
155
  return;
173
156
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-7KGB7GSZ.js";
3
+ } from "./chunk-GZ7QPRKV.js";
4
4
  import {
5
5
  copyToClipboard,
6
6
  getMcpEntry,
@@ -9,11 +9,12 @@ import {
9
9
  import {
10
10
  skillsCombined
11
11
  } from "./chunk-4QBEOJPX.js";
12
- import "./chunk-SNSGVRCU.js";
12
+ import {
13
+ runOAuthCallback
14
+ } from "./chunk-3XRFIJUG.js";
13
15
 
14
16
  // src/install.ts
15
17
  import { createInterface } from "readline/promises";
16
- import { createServer } from "http";
17
18
  import open from "open";
18
19
 
19
20
  // src/installers/registry.ts
@@ -196,12 +197,15 @@ function jsonMcpServersAdapter(spec) {
196
197
  }
197
198
 
198
199
  // src/installers/web-client.ts
200
+ var DEFAULT_REMOTE_MCP_URL = "https://mcp.plaud.ai/mcp";
199
201
  function remoteMcpUrl() {
200
202
  const explicit = process.env.PLAUD_REMOTE_MCP_URL?.trim();
201
203
  if (explicit) return explicit;
202
204
  const serverUrl = process.env.PLAUD_SERVER_URL?.trim();
203
- if (!serverUrl) return null;
204
- return /\/(mcp|sse)\/?$/.test(serverUrl) ? serverUrl : `${serverUrl.replace(/\/+$/, "")}/mcp`;
205
+ if (serverUrl) {
206
+ return /\/(mcp|sse)\/?$/.test(serverUrl) ? serverUrl : `${serverUrl.replace(/\/+$/, "")}/mcp`;
207
+ }
208
+ return DEFAULT_REMOTE_MCP_URL;
205
209
  }
206
210
  function webClientAdapter(spec) {
207
211
  return {
@@ -213,7 +217,6 @@ function webClientAdapter(spec) {
213
217
  },
214
218
  async install() {
215
219
  const url = remoteMcpUrl();
216
- const remoteLine = url ? `Remote MCP URL: ${url}` : "Remote MCP URL: not configured. Deploy `plaud-mcp http` behind HTTPS, then rerun with PLAUD_REMOTE_MCP_URL=https://your-host.example/mcp.";
217
220
  return {
218
221
  status: "configured",
219
222
  requiresLocalAuth: false,
@@ -221,7 +224,7 @@ function webClientAdapter(spec) {
221
224
  message: [
222
225
  "manual setup only:",
223
226
  ` Open: ${spec.setupUrl}`,
224
- ` ${remoteLine}`,
227
+ ` Remote MCP URL: ${url}`,
225
228
  ` Step: ${spec.setupStep}`
226
229
  ].join("\n")
227
230
  };
@@ -408,71 +411,53 @@ async function runLogin() {
408
411
  try {
409
412
  const user = await client.getCurrentUser();
410
413
  return { status: "already-authed", who: pickIdentity(user) };
411
- } catch {
412
- return { status: "already-authed" };
414
+ } catch (err) {
415
+ const msg = err instanceof Error ? err.message : String(err);
416
+ if (msg.includes("401") || msg.includes("Not authenticated")) {
417
+ await client.auth.logout().catch(() => void 0);
418
+ } else {
419
+ return { status: "already-authed" };
420
+ }
413
421
  }
414
422
  }
415
423
  } catch {
416
424
  await client.auth.logout().catch(() => void 0);
417
425
  }
418
426
  const { url, codeVerifier, state } = client.auth.createAuthorizationRequest();
419
- return new Promise((resolve) => {
420
- const httpServer = createServer(async (req, res) => {
421
- const reqUrl = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
422
- if (reqUrl.pathname !== "/auth/callback") {
423
- res.writeHead(404);
424
- res.end();
425
- return;
426
- }
427
- const code = reqUrl.searchParams.get("code");
428
- if (!code) {
429
- res.writeHead(400);
430
- res.end("Missing code");
431
- cleanup();
432
- resolve({ status: "failed", message: "missing authorization code in callback" });
433
- return;
434
- }
435
- try {
436
- await client.auth.exchangeCode(code, codeVerifier, state);
437
- res.writeHead(200, { "Content-Type": "text/html" });
438
- res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
439
- cleanup();
440
- let who;
441
- try {
442
- const user = await client.getCurrentUser();
443
- who = pickIdentity(user);
444
- } catch {
445
- }
446
- resolve({ status: "success", who });
447
- } catch (err) {
448
- res.writeHead(500, { "Content-Type": "text/html" });
449
- res.end("<h1>Token exchange failed</h1>");
450
- cleanup();
451
- resolve({ status: "failed", message: err instanceof Error ? err.message : String(err) });
452
- }
453
- });
454
- let timeoutId;
455
- function cleanup() {
456
- clearTimeout(timeoutId);
457
- httpServer.closeAllConnections?.();
458
- httpServer.close();
459
- }
460
- timeoutId = setTimeout(() => {
461
- cleanup();
462
- resolve({ status: "timeout" });
463
- }, LOGIN_TIMEOUT_MS);
464
- httpServer.listen(CALLBACK_PORT, () => {
427
+ const result = await runOAuthCallback({
428
+ port: CALLBACK_PORT,
429
+ expectedState: state,
430
+ timeoutMs: LOGIN_TIMEOUT_MS,
431
+ exchangeCode: async (code) => {
432
+ await client.auth.exchangeCode(code, codeVerifier, state);
433
+ },
434
+ onListening: () => {
465
435
  console.log(` if no browser opens, open this URL while this command is still running:
466
436
  ${url}`);
467
437
  open(url).catch(() => {
468
438
  console.log(" browser launch failed \u2014 waiting for a manual browser callback.");
469
439
  });
470
- });
471
- httpServer.on("error", (err) => {
472
- cleanup();
473
- resolve({ status: "failed", message: `callback server error: ${err.message}` });
474
- });
440
+ }
475
441
  });
442
+ switch (result.status) {
443
+ case "success": {
444
+ let who;
445
+ try {
446
+ const user = await client.getCurrentUser();
447
+ who = pickIdentity(user);
448
+ } catch {
449
+ }
450
+ return { status: "success", who };
451
+ }
452
+ case "timeout":
453
+ return { status: "timeout" };
454
+ case "denied":
455
+ return { status: "failed", message: result.error?.message ?? "authorization denied" };
456
+ case "exchange-failed":
457
+ return { status: "failed", message: result.error?.message ?? "token exchange failed" };
458
+ case "listen-failed":
459
+ return { status: "failed", message: result.error?.message ?? "callback server failed to start" };
460
+ }
476
461
  }
477
462
  async function runInstall(opts = {}) {
478
463
  console.log("Plaud MCP installer\n");
@@ -554,8 +539,14 @@ async function doLoginStep(nonInteractive) {
554
539
  const who = pickIdentity(user);
555
540
  console.log(` already signed in${who ? ` as ${who}` : ""} \u2014 skipping OAuth.`);
556
541
  return { status: "already-authed", who };
557
- } catch {
558
- console.log(" token present but user lookup failed \u2014 will re-auth.");
542
+ } catch (err) {
543
+ const msg = err instanceof Error ? err.message : String(err);
544
+ if (msg.includes("401") || msg.includes("Not authenticated")) {
545
+ console.log(" saved token was revoked server-side \u2014 clearing and re-authenticating.");
546
+ await client.auth.logout().catch(() => void 0);
547
+ } else {
548
+ console.log(" token present but user lookup failed \u2014 will re-auth.");
549
+ }
559
550
  }
560
551
  }
561
552
  } catch {