@plaud-ai/mcp 0.2.2 → 0.2.3

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.3"
21
22
  });
22
23
  var CALLBACK_PORT = 8199;
23
24
  var LOGIN_TIMEOUT_MS = 12e4;
@@ -30,79 +31,48 @@ server.registerTool("login", {
30
31
  return { content: [{ type: "text", text: "Already logged in." }] };
31
32
  }
32
33
  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}` }],
67
- isError: true
68
- });
69
- }
70
- });
71
- let timeoutId;
72
- function cleanup() {
73
- clearTimeout(timeoutId);
74
- httpServer.closeAllConnections?.();
75
- httpServer.close();
76
- }
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, () => {
34
+ const result = await runOAuthCallback({
35
+ port: CALLBACK_PORT,
36
+ expectedState: state,
37
+ timeoutMs: LOGIN_TIMEOUT_MS,
38
+ exchangeCode: async (code) => {
39
+ await client.auth.exchangeCode(code, codeVerifier, state);
40
+ },
41
+ onListening: () => {
85
42
  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:
43
+ });
44
+ }
45
+ });
46
+ switch (result.status) {
47
+ case "success":
48
+ return { content: [{ type: "text", text: "Successfully authenticated with Plaud!" }] };
49
+ case "timeout":
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: `Authentication timed out after 2 minutes. If no browser opened, open this URL on the same machine and retry:
90
54
  ${url}
91
55
 
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}` }],
56
+ If this is a remote/headless machine, forward local port ${CALLBACK_PORT} first.`
57
+ }],
102
58
  isError: true
103
- });
104
- });
105
- });
59
+ };
60
+ case "denied":
61
+ return {
62
+ content: [{ type: "text", text: `Authentication denied: ${result.error?.message ?? "user declined authorization"}` }],
63
+ isError: true
64
+ };
65
+ case "exchange-failed":
66
+ return {
67
+ content: [{ type: "text", text: `Authentication failed: ${result.error?.message ?? "token exchange failed"}` }],
68
+ isError: true
69
+ };
70
+ case "listen-failed":
71
+ return {
72
+ content: [{ type: "text", text: `Failed to start callback server: ${result.error?.message ?? "unknown error"}` }],
73
+ isError: true
74
+ };
75
+ }
106
76
  });
107
77
  registerTools(server, getClient());
108
78
  server.registerTool("logout", {
@@ -134,7 +104,7 @@ async function main() {
134
104
  const sub = process.argv[2];
135
105
  const sub2 = process.argv[3];
136
106
  if (sub === "install") {
137
- const { runInstall } = await import("./install-EEQXUUG3.js");
107
+ const { runInstall } = await import("./install-C7WMVIPG.js");
138
108
  const args = process.argv.slice(3);
139
109
  const yes = args.some((a) => a === "--yes" || a === "-y");
140
110
  const noLogin = args.some((a) => a === "--no-login");
@@ -167,7 +137,7 @@ async function main() {
167
137
  return;
168
138
  }
169
139
  if (sub === "http") {
170
- const { startHttpServer } = await import("./server-QWAZPBHU.js");
140
+ const { startHttpServer } = await import("./server-ORMBAVW6.js");
171
141
  startHttpServer();
172
142
  return;
173
143
  }
@@ -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
  };
@@ -416,63 +419,40 @@ async function runLogin() {
416
419
  await client.auth.logout().catch(() => void 0);
417
420
  }
418
421
  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, () => {
422
+ const result = await runOAuthCallback({
423
+ port: CALLBACK_PORT,
424
+ expectedState: state,
425
+ timeoutMs: LOGIN_TIMEOUT_MS,
426
+ exchangeCode: async (code) => {
427
+ await client.auth.exchangeCode(code, codeVerifier, state);
428
+ },
429
+ onListening: () => {
465
430
  console.log(` if no browser opens, open this URL while this command is still running:
466
431
  ${url}`);
467
432
  open(url).catch(() => {
468
433
  console.log(" browser launch failed \u2014 waiting for a manual browser callback.");
469
434
  });
470
- });
471
- httpServer.on("error", (err) => {
472
- cleanup();
473
- resolve({ status: "failed", message: `callback server error: ${err.message}` });
474
- });
435
+ }
475
436
  });
437
+ switch (result.status) {
438
+ case "success": {
439
+ let who;
440
+ try {
441
+ const user = await client.getCurrentUser();
442
+ who = pickIdentity(user);
443
+ } catch {
444
+ }
445
+ return { status: "success", who };
446
+ }
447
+ case "timeout":
448
+ return { status: "timeout" };
449
+ case "denied":
450
+ return { status: "failed", message: result.error?.message ?? "authorization denied" };
451
+ case "exchange-failed":
452
+ return { status: "failed", message: result.error?.message ?? "token exchange failed" };
453
+ case "listen-failed":
454
+ return { status: "failed", message: result.error?.message ?? "callback server failed to start" };
455
+ }
476
456
  }
477
457
  async function runInstall(opts = {}) {
478
458
  console.log("Plaud MCP installer\n");