@plaud-ai/mcp 0.3.8 → 0.3.9
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.
|
@@ -21,7 +21,29 @@ var toolHooks = null;
|
|
|
21
21
|
var MAX_FILTER_PAGES = 5;
|
|
22
22
|
var FILTER_PAGE_SIZE = 100;
|
|
23
23
|
var TRANSCRIPT_PAGE_SIZE = 50;
|
|
24
|
-
var TRANSCRIPT_BLOCKS = ["transaction", "outline", "transaction_polish"];
|
|
24
|
+
var TRANSCRIPT_BLOCKS = ["transaction", "outline", "transaction_polish", "mark_memo"];
|
|
25
|
+
var BLOCK_ITEMS_KEY = { mark_memo: "marks" };
|
|
26
|
+
function linkBackedTypes(list) {
|
|
27
|
+
return (list ?? []).filter(
|
|
28
|
+
(b) => !(typeof b.data_content === "string" && b.data_content.length > 0) && typeof b.data_link === "string" && b.data_link.length > 0
|
|
29
|
+
).map((b) => typeof b.data_type === "string" ? b.data_type : "(untyped)");
|
|
30
|
+
}
|
|
31
|
+
async function resolveNotes(noteList) {
|
|
32
|
+
return Promise.all(
|
|
33
|
+
noteList.map(async (note) => {
|
|
34
|
+
if (typeof note.data_content === "string" && note.data_content.length > 0) return note;
|
|
35
|
+
if (typeof note.data_link !== "string" || note.data_link.length === 0) return note;
|
|
36
|
+
try {
|
|
37
|
+
return { ...note, data_content: await loadBlockContent(note) };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
...note,
|
|
41
|
+
data_content_error: err instanceof Error ? err.message : "Failed to fetch content from data_link"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
25
47
|
function encodeTranscriptCursor(offset) {
|
|
26
48
|
return Buffer.from(JSON.stringify({ o: offset }), "utf8").toString("base64url");
|
|
27
49
|
}
|
|
@@ -175,6 +197,18 @@ function registerTools(server, client, hooks) {
|
|
|
175
197
|
const synced = !!(file.duration || file.source_list && file.source_list.length);
|
|
176
198
|
text += synced ? "\n\nNote: `presigned_url` (audio download URL) is null for this otherwise-synced recording. This is usually a transient backend signing issue \u2014 retry get_file in a few minutes to obtain the URL." : "\n\nNote: `presigned_url` (audio download URL) is null \u2014 audio may not yet be available for this recording.";
|
|
177
199
|
}
|
|
200
|
+
const linkedBlocks = linkBackedTypes(file.source_list);
|
|
201
|
+
const linkedNotes = linkBackedTypes(file.note_list);
|
|
202
|
+
if (linkedBlocks.length) {
|
|
203
|
+
text += `
|
|
204
|
+
|
|
205
|
+
Note: source_list ${linkedBlocks.map((t) => `\`${t}\``).join(", ")} returned an empty \`data_content\` \u2014 the body lives behind \`data_link\`, not missing. Call get_transcript with the matching \`block\` to read it (it fetches the link).`;
|
|
206
|
+
}
|
|
207
|
+
if (linkedNotes.length) {
|
|
208
|
+
text += `
|
|
209
|
+
|
|
210
|
+
Note: note_list ${linkedNotes.map((t) => `\`${t}\``).join(", ")} returned an empty \`data_content\` \u2014 the body lives behind \`data_link\`, not missing. Call get_note to read it (it fetches the link).`;
|
|
211
|
+
}
|
|
178
212
|
recordToolMetric("get_file", "success", Date.now() - start, requestId, { file_id });
|
|
179
213
|
logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start, presigned_url_present: !!file.presigned_url });
|
|
180
214
|
return { content: [{ type: "text", text }] };
|
|
@@ -188,7 +222,7 @@ function registerTools(server, client, hooks) {
|
|
|
188
222
|
server.registerTool(
|
|
189
223
|
"get_note",
|
|
190
224
|
{
|
|
191
|
-
description: "Fetch
|
|
225
|
+
description: "Fetch the notes on a Plaud recording \u2014 one entry per tab in the app, so usually more than one: the AI summary (action items, key topics), any template tab, a saved Ask Plaud answer, and the highlights note when the user pressed the highlight button while recording.",
|
|
192
226
|
annotations: {
|
|
193
227
|
title: "Get recording notes",
|
|
194
228
|
readOnlyHint: true,
|
|
@@ -203,9 +237,10 @@ function registerTools(server, client, hooks) {
|
|
|
203
237
|
const requestId = emitToolClick("get_note", { file_id });
|
|
204
238
|
try {
|
|
205
239
|
const file = await client.getFile(file_id);
|
|
240
|
+
const notes = await resolveNotes(file.note_list ?? []);
|
|
206
241
|
recordToolMetric("get_note", "success", Date.now() - start, requestId, { file_id });
|
|
207
|
-
logger.info({ event: "tool_call_end", tool: "get_note", duration_ms: Date.now() - start });
|
|
208
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
242
|
+
logger.info({ event: "tool_call_end", tool: "get_note", duration_ms: Date.now() - start, notes: notes.length });
|
|
243
|
+
return { content: [{ type: "text", text: JSON.stringify(notes, null, 2) }] };
|
|
209
244
|
} catch (err) {
|
|
210
245
|
recordToolMetric("get_note", "error", Date.now() - start, requestId, { file_id }, err);
|
|
211
246
|
logger.error({ event: "tool_call_error", tool: "get_note", duration_ms: Date.now() - start, error: String(err) });
|
|
@@ -216,7 +251,7 @@ function registerTools(server, client, hooks) {
|
|
|
216
251
|
server.registerTool(
|
|
217
252
|
"get_transcript",
|
|
218
253
|
{
|
|
219
|
-
description: "Fetch the timestamped transcript with speaker attribution for a Plaud recording. Defaults to the `transaction` block (real speaker names + timestamps), returned one page of utterances at a time to stay within client size limits \u2014 call again with the returned `next_cursor` to fetch the next page. Set `block` to `outline` or `
|
|
254
|
+
description: "Fetch the timestamped transcript with speaker attribution for a Plaud recording. Defaults to the `transaction` block (real speaker names + timestamps), returned one page of utterances at a time to stay within client size limits \u2014 call again with the returned `next_cursor` to fetch the next page. Set `block` to `outline`, `transaction_polish`, or `mark_memo` (the moments the user flagged with the device's highlight button) to fetch those blocks instead.",
|
|
220
255
|
annotations: {
|
|
221
256
|
title: "Get recording transcript",
|
|
222
257
|
readOnlyHint: true,
|
|
@@ -226,10 +261,10 @@ function registerTools(server, client, hooks) {
|
|
|
226
261
|
inputSchema: {
|
|
227
262
|
file_id: z.string().describe("The file ID to retrieve transcript for"),
|
|
228
263
|
block: z.enum(TRANSCRIPT_BLOCKS).optional().describe(
|
|
229
|
-
"Which source block to fetch: `transaction` (default; raw transcript, speaker + timestamps), `transaction_polish` (AI-cleaned transcript; same per-utterance shape, keeps speaker + timestamps), or `
|
|
264
|
+
"Which source block to fetch: `transaction` (default; raw transcript, speaker + timestamps), `transaction_polish` (AI-cleaned transcript; same per-utterance shape, keeps speaker + timestamps), `outline`, or `mark_memo` (highlights: the moments flagged by pressing the button on the device during recording; present only when the button was pressed, and returned under `marks`). Pair `mark_memo` with a `transaction` call to map each flagged moment onto what was said there."
|
|
230
265
|
),
|
|
231
266
|
cursor: z.string().optional().describe("Opaque pagination cursor from a previous call's `next_cursor`. Omit to start from the first utterance."),
|
|
232
|
-
limit: z.number().int().min(1).max(500).optional().describe(`Max
|
|
267
|
+
limit: z.number().int().min(1).max(500).optional().describe(`Max items to return in this page (default ${TRANSCRIPT_PAGE_SIZE}). Only applies to list-shaped blocks.`)
|
|
233
268
|
}
|
|
234
269
|
},
|
|
235
270
|
async ({ file_id, block, cursor, limit }) => {
|
|
@@ -267,7 +302,7 @@ function registerTools(server, client, hooks) {
|
|
|
267
302
|
if (!segments) {
|
|
268
303
|
recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
|
|
269
304
|
logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, paginated: false });
|
|
270
|
-
const text = content || `Block "${blockType}" has no content for this recording yet
|
|
305
|
+
const text = content || (blockType === "mark_memo" ? "This recording has a highlights block, but it is empty \u2014 no highlight was recorded." : `Block "${blockType}" has no content for this recording yet.`);
|
|
271
306
|
return { content: [{ type: "text", text }] };
|
|
272
307
|
}
|
|
273
308
|
let offset = 0;
|
|
@@ -295,7 +330,7 @@ function registerTools(server, client, hooks) {
|
|
|
295
330
|
limit: pageSize,
|
|
296
331
|
returned: page.length,
|
|
297
332
|
next_cursor: hasMore ? encodeTranscriptCursor(nextOffset) : null,
|
|
298
|
-
segments: page
|
|
333
|
+
[BLOCK_ITEMS_KEY[blockType] ?? "segments"]: page
|
|
299
334
|
};
|
|
300
335
|
recordToolMetric("get_transcript", "success", Date.now() - start, requestId, { file_id });
|
|
301
336
|
logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start, block: blockType, paginated: true, total: segments.length, offset, returned: page.length });
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
normalizeMcpHost,
|
|
10
10
|
registerTools
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FSNFNJXH.js";
|
|
12
12
|
import {
|
|
13
13
|
capture,
|
|
14
14
|
classifyError,
|
|
@@ -31,7 +31,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
31
31
|
import open from "open";
|
|
32
32
|
var server = new McpServer({
|
|
33
33
|
name: "plaud",
|
|
34
|
-
version: "0.3.
|
|
34
|
+
version: "0.3.9"
|
|
35
35
|
});
|
|
36
36
|
var CALLBACK_PORT = 8199;
|
|
37
37
|
var LOGIN_TIMEOUT_MS = 12e4;
|
|
@@ -191,7 +191,7 @@ async function main() {
|
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
if (sub === "http") {
|
|
194
|
-
const { startHttpServer } = await import("./server-
|
|
194
|
+
const { startHttpServer } = await import("./server-SYSE62NO.js");
|
|
195
195
|
const { startMetricsServer } = await import("./server-NKCNUA6P.js");
|
|
196
196
|
startMetricsServer();
|
|
197
197
|
startHttpServer();
|
|
@@ -217,7 +217,7 @@ Usage:
|
|
|
217
217
|
try {
|
|
218
218
|
await initTelemetry({
|
|
219
219
|
surface: "mcp",
|
|
220
|
-
appVersion: "0.3.
|
|
220
|
+
appVersion: "0.3.9",
|
|
221
221
|
transport: "stdio"
|
|
222
222
|
});
|
|
223
223
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeMcpHost,
|
|
3
3
|
registerTools
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FSNFNJXH.js";
|
|
5
5
|
import {
|
|
6
6
|
PlaudClient
|
|
7
7
|
} from "./chunk-5NWKLF3V.js";
|
|
@@ -276,6 +276,27 @@ function subFromJwt(token) {
|
|
|
276
276
|
return void 0;
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
+
var AUTO_RECOVERED_CLIENT_NAME = "auto-recovered client";
|
|
280
|
+
var REDIRECT_HOST_CLIENT_NAMES = {
|
|
281
|
+
"chatgpt.com": "chatgpt",
|
|
282
|
+
"openai.com": "chatgpt",
|
|
283
|
+
"claude.ai": "claude",
|
|
284
|
+
"claude.com": "claude"
|
|
285
|
+
};
|
|
286
|
+
function clientNameFromRedirectUri(redirectUri) {
|
|
287
|
+
if (!redirectUri) return void 0;
|
|
288
|
+
let host;
|
|
289
|
+
try {
|
|
290
|
+
host = new URL(redirectUri).hostname.toLowerCase().replace(/^www\./, "");
|
|
291
|
+
} catch {
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
if (!host) return void 0;
|
|
295
|
+
for (const [domain, name] of Object.entries(REDIRECT_HOST_CLIENT_NAMES)) {
|
|
296
|
+
if (host === domain || host.endsWith(`.${domain}`)) return name;
|
|
297
|
+
}
|
|
298
|
+
return host;
|
|
299
|
+
}
|
|
279
300
|
var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
|
|
280
301
|
var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
|
|
281
302
|
var STATELESS_CODE_PREFIX = "pc1_";
|
|
@@ -532,12 +553,25 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
532
553
|
if (redirectUri && !existing.redirect_uris.includes(redirectUri)) {
|
|
533
554
|
existing.redirect_uris = [...existing.redirect_uris, redirectUri];
|
|
534
555
|
}
|
|
556
|
+
if (existing.client_name === AUTO_RECOVERED_CLIENT_NAME) {
|
|
557
|
+
const upgradedName = clientNameFromRedirectUri(redirectUri);
|
|
558
|
+
if (upgradedName) {
|
|
559
|
+
existing.client_name = upgradedName;
|
|
560
|
+
logger.info({
|
|
561
|
+
event: "oauth_client_name_upgraded",
|
|
562
|
+
client_id: clientId,
|
|
563
|
+
name_source: "redirect_uri",
|
|
564
|
+
mcp_host: normalizeMcpHost(upgradedName) ?? null
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
535
568
|
return true;
|
|
536
569
|
}
|
|
537
570
|
if (!this.verifyClientIdSignature(clientId)) {
|
|
538
571
|
return false;
|
|
539
572
|
}
|
|
540
573
|
const recoveredName = this.decodeClientName(clientId);
|
|
574
|
+
const derivedName = recoveredName ?? clientNameFromRedirectUri(redirectUri);
|
|
541
575
|
this._registeredClients.set(clientId, {
|
|
542
576
|
client_id: clientId,
|
|
543
577
|
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
@@ -545,13 +579,20 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
545
579
|
grant_types: ["authorization_code", "refresh_token"],
|
|
546
580
|
response_types: ["code"],
|
|
547
581
|
token_endpoint_auth_method: "none",
|
|
548
|
-
client_name:
|
|
582
|
+
client_name: derivedName ?? AUTO_RECOVERED_CLIENT_NAME
|
|
549
583
|
});
|
|
550
584
|
logger.info({
|
|
551
585
|
event: "oauth_client_recovered",
|
|
552
586
|
client_id: clientId,
|
|
553
587
|
redirect_uri: redirectUri ?? null,
|
|
554
|
-
|
|
588
|
+
// Unchanged semantics: true only when the name came out of the client_id
|
|
589
|
+
// itself — this is the field used to verify the #72 fix in prod.
|
|
590
|
+
name_recovered: recoveredName != null,
|
|
591
|
+
name_source: recoveredName != null ? "client_id" : derivedName != null ? "redirect_uri" : null,
|
|
592
|
+
// The bucket this recovery will put on every downstream auth event. Lets us
|
|
593
|
+
// verify attribution straight from prod logs (the warehouse events are
|
|
594
|
+
// batched, so they're not a live signal), and enumerate real hosts.
|
|
595
|
+
mcp_host: normalizeMcpHost(derivedName) ?? null
|
|
555
596
|
});
|
|
556
597
|
return true;
|
|
557
598
|
}
|
|
@@ -600,7 +641,9 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
600
641
|
res.redirect(targetUrl.toString());
|
|
601
642
|
}
|
|
602
643
|
/**
|
|
603
|
-
* Called when Plaud redirects to our /
|
|
644
|
+
* Called when Plaud redirects to our /auth/callback (CALLBACK_PATH in
|
|
645
|
+
* http/server.ts). NOT /oauth/callback — this comment said so until 2026-08-19
|
|
646
|
+
* and a downstream client registration was configured from it by mistake.
|
|
604
647
|
* Looks up the original client redirect_uri and forwards the code+state to it.
|
|
605
648
|
*/
|
|
606
649
|
handleCallback(code, state, res) {
|
|
@@ -1183,6 +1226,7 @@ var OPENAI_APPS_CHALLENGE_TOKEN = "bBlB7N7fF7YaQnONtEQNAGP7SLaqpJT3a3DS5Zv1F8s";
|
|
|
1183
1226
|
var OAUTH_DEBUG_LOGS = !["0", "false", "no", "off"].includes(
|
|
1184
1227
|
(process.env.PLAUD_OAUTH_DEBUG_LOGS ?? "").toLowerCase()
|
|
1185
1228
|
);
|
|
1229
|
+
var ROOT_REDIRECT_URL = process.env.PLAUD_ROOT_REDIRECT_URL ?? "https://www.plaud.ai";
|
|
1186
1230
|
var CIMD_ENABLED = ["1", "true", "yes", "on"].includes(
|
|
1187
1231
|
(process.env.PLAUD_CIMD_ENABLED ?? "").toLowerCase()
|
|
1188
1232
|
);
|
|
@@ -1239,8 +1283,8 @@ function startHttpServer() {
|
|
|
1239
1283
|
common: {
|
|
1240
1284
|
serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
|
|
1241
1285
|
// matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
|
|
1242
|
-
serviceVersion: "0.3.
|
|
1243
|
-
buildId: "
|
|
1286
|
+
serviceVersion: "0.3.9",
|
|
1287
|
+
buildId: "ee2ff6d",
|
|
1244
1288
|
// mcp tsup TODO: inject git short SHA (like CLI)
|
|
1245
1289
|
region: process.env.PLAUD_REGION ?? "US",
|
|
1246
1290
|
env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
|
|
@@ -1333,6 +1377,9 @@ function startHttpServer() {
|
|
|
1333
1377
|
}
|
|
1334
1378
|
});
|
|
1335
1379
|
});
|
|
1380
|
+
app.get("/", (_req, res) => {
|
|
1381
|
+
res.redirect(302, ROOT_REDIRECT_URL);
|
|
1382
|
+
});
|
|
1336
1383
|
if (process.env.PLAUD_CALLBACK_URL) {
|
|
1337
1384
|
app.get(CALLBACK_PATH, (req, res) => {
|
|
1338
1385
|
const code = req.query["code"];
|
|
@@ -1509,7 +1556,7 @@ function startHttpServer() {
|
|
|
1509
1556
|
apiBase,
|
|
1510
1557
|
staticToken: token
|
|
1511
1558
|
});
|
|
1512
|
-
const mcpServer = new McpServer({ name: "plaud", version: "0.3.
|
|
1559
|
+
const mcpServer = new McpServer({ name: "plaud", version: "0.3.9" });
|
|
1513
1560
|
registerTools(mcpServer, client, warehouseToolHooks);
|
|
1514
1561
|
const transport = new StreamableHTTPServerTransport({
|
|
1515
1562
|
sessionIdGenerator: void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plaud-ai/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"plugin.json",
|
|
12
11
|
".mcp.json",
|
|
13
12
|
"skills"
|
|
14
13
|
],
|
|
@@ -18,7 +17,7 @@
|
|
|
18
17
|
"build": "tsup",
|
|
19
18
|
"dev": "tsup --watch",
|
|
20
19
|
"clean": "rm -rf dist skills",
|
|
21
|
-
"prepublishOnly": "node -e \"const fs=require('fs'),v=require('./package.json').version,p=JSON.parse(fs.readFileSync(
|
|
20
|
+
"prepublishOnly": "node -e \"const fs=require('fs'),v=require('./package.json').version,f='../../plugin.json',p=JSON.parse(fs.readFileSync(f,'utf8'));p.version=v;fs.writeFileSync(f,JSON.stringify(p,null,2)+'\\n');\""
|
|
22
21
|
},
|
|
23
22
|
"dependencies": {
|
|
24
23
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plaud-read
|
|
3
3
|
version: 1.0.0
|
|
4
|
-
description: "Read the transcript, AI summary, notes, or download audio for a specific Plaud recording. Use when the user says 'show the transcript', 'summarize this', 'what was said', 'get audio', 'the notes from', or names a specific recording to dig into. Also covers extracting structured fields from a recording."
|
|
4
|
+
description: "Read the transcript, AI summary, notes, highlights, or download audio for a specific Plaud recording. Use when the user says 'show the transcript', 'summarize this', 'what was said', 'get audio', 'the notes from', 'my highlights', 'the moments I flagged', 'what I marked while recording', or names a specific recording to dig into. Also covers extracting structured fields from a recording."
|
|
5
5
|
metadata:
|
|
6
6
|
requires:
|
|
7
7
|
bins: []
|
|
@@ -14,7 +14,7 @@ metadata:
|
|
|
14
14
|
## When to use
|
|
15
15
|
|
|
16
16
|
- User names a specific recording (by name or ID) and wants to read its content.
|
|
17
|
-
- User asks for "transcript", "summary", "action items", "audio", "who said what", or a structured extraction ("action items, decisions, attendees").
|
|
17
|
+
- User asks for "transcript", "summary", "action items", "audio", "who said what", "my highlights" / "the moments I flagged", or a structured extraction ("action items, decisions, attendees").
|
|
18
18
|
- If the user did **not** specify a recording, hand off to `plaud-find` (by topic) or `plaud-browse` (by recency) first.
|
|
19
19
|
|
|
20
20
|
## Tool selection matrix
|
|
@@ -23,8 +23,9 @@ metadata:
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| AI summary, TL;DR, action items | `get_note` | Returns Markdown; usually enough — try this before `get_transcript` |
|
|
25
25
|
| Verbatim quotes, full dialogue | `get_transcript` | Timestamped; paginated — follow `next_cursor` for long recordings |
|
|
26
|
+
| Highlights — "the moments I flagged", "what I marked while recording" | `get_transcript` with `block: "mark_memo"` | Returned under `marks`. Exists only if the user pressed the device's highlight button; also surfaced as the `high_light` note by `get_note`. To turn marks into quotes, pair with a `transaction` call and match on the timestamps |
|
|
26
27
|
| Audio download link | `get_file` then use `presigned_url` | Link expires in 24h |
|
|
27
|
-
| Full metadata + availability flags | `get_file` |
|
|
28
|
+
| Full metadata + availability flags | `get_file` | Lists which blocks exist. **An entry with `data_content: ""` and a `data_link` is not empty** — its body is link-backed; read it with `get_transcript` / `get_note`, which resolve the link. Never report such a block as missing data |
|
|
28
29
|
|
|
29
30
|
## Structured extraction workflow
|
|
30
31
|
|
|
@@ -19,15 +19,31 @@ metadata:
|
|
|
19
19
|
|
|
20
20
|
## Tool inventory
|
|
21
21
|
|
|
22
|
-
| Tool | Purpose |
|
|
23
|
-
|
|
24
|
-
| `login` | Open browser for OAuth; blocks until callback or 2-min timeout |
|
|
25
|
-
| `logout` | Revoke and clear tokens |
|
|
26
|
-
| `get_current_user` | Verify who is signed in |
|
|
27
|
-
| `list_files` | Browse, paginate, filter recordings (supports `query`, `date_from`, `date_to`) |
|
|
28
|
-
| `get_file` | Full record incl. `presigned_url`, `source_list`, `note_list` |
|
|
29
|
-
| `get_note` | AI-generated summary and action items |
|
|
30
|
-
| `get_transcript` | Timestamped transcript with speaker labels |
|
|
22
|
+
| Tool | Purpose | Local | Remote |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `login` | Open browser for OAuth; blocks until callback or 2-min timeout | ✅ | — |
|
|
25
|
+
| `logout` | Revoke and clear tokens | ✅ | — |
|
|
26
|
+
| `get_current_user` | Verify who is signed in | ✅ | ✅ |
|
|
27
|
+
| `list_files` | Browse, paginate, filter recordings (supports `query`, `date_from`, `date_to`) | ✅ | ✅ |
|
|
28
|
+
| `get_file` | Full record incl. `presigned_url`, `source_list`, `note_list` | ✅ | ✅ |
|
|
29
|
+
| `get_note` | AI-generated summary and action items | ✅ | ✅ |
|
|
30
|
+
| `get_transcript` | Timestamped transcript with speaker labels | ✅ | ✅ |
|
|
31
|
+
|
|
32
|
+
**Local** = the stdio server (`npx @plaud-ai/mcp`). **Remote** = a hosted connector
|
|
33
|
+
the host dials by URL. Remote has no `login` / `logout`: authorization runs as a
|
|
34
|
+
browser OAuth redirect driven by the host, not as a tool call. On a remote
|
|
35
|
+
connector, treat "sign in" as something the user does in the host's UI.
|
|
36
|
+
|
|
37
|
+
### Tool names may carry a host-applied prefix
|
|
38
|
+
|
|
39
|
+
The names above are the canonical ones the server registers. Some hosts namespace
|
|
40
|
+
them with the connector name the user chose. WorkBuddy, for example, turns a
|
|
41
|
+
connector named `plaudcn` into `plaudcn_list_files`, `plaudcn_get_transcript`, and
|
|
42
|
+
so on — the prefix follows the user's own config, so it is not predictable.
|
|
43
|
+
|
|
44
|
+
**Always resolve tools from the list you were actually given, matching on the
|
|
45
|
+
canonical suffix** (`…list_files`, `…get_transcript`). Never assume a bare name is
|
|
46
|
+
callable, and never hard-code a prefix.
|
|
31
47
|
|
|
32
48
|
## Error semantics
|
|
33
49
|
|