@plaud-ai/mcp 0.1.58 → 0.2.1
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.
- package/dist/chunk-4QBEOJPX.js +92 -0
- package/dist/chunk-7KGB7GSZ.js +33 -0
- package/dist/chunk-K22WPZ7P.js +79 -0
- package/dist/chunk-MPCF6HMK.js +151 -0
- package/dist/{chunk-YMQLKZLW.js → chunk-SNSGVRCU.js} +1 -101
- package/dist/index.js +70 -60
- package/dist/install-I2N3DJG4.js +384 -0
- package/dist/{server-5L7BYLYR.js → server-D3H6AMVD.js} +4 -14
- package/dist/{setup-KGUMS567.js → setup-QRUDA7ES.js} +17 -61
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/plaud-browse/SKILL.md +39 -0
- package/skills/plaud-digest/SKILL.md +41 -0
- package/skills/plaud-export/SKILL.md +51 -0
- package/skills/plaud-find/SKILL.md +54 -0
- package/skills/plaud-followup/SKILL.md +59 -0
- package/skills/plaud-read/SKILL.md +51 -0
- package/skills/plaud-shared/SKILL.md +67 -0
- package/dist/chunk-7PYQFJJW.js +0 -111
- package/skills/plaud/SKILL.md +0 -40
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/skills.ts
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { statSync } from "fs";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function parseFrontmatter(raw) {
|
|
7
|
+
if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
|
|
8
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
9
|
+
if (end === -1) return { meta: {}, body: raw };
|
|
10
|
+
const yaml = raw.slice(4, end);
|
|
11
|
+
const body = raw.slice(end + 5);
|
|
12
|
+
const meta = {};
|
|
13
|
+
let currentKey = null;
|
|
14
|
+
for (const line of yaml.split("\n")) {
|
|
15
|
+
if (!line.trim()) continue;
|
|
16
|
+
const topMatch = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
|
|
17
|
+
if (topMatch && !line.startsWith(" ")) {
|
|
18
|
+
const [, k, v] = topMatch;
|
|
19
|
+
currentKey = k;
|
|
20
|
+
if (v !== "") {
|
|
21
|
+
const unq = v.replace(/^["']|["']$/g, "");
|
|
22
|
+
meta[k] = unq;
|
|
23
|
+
currentKey = null;
|
|
24
|
+
} else {
|
|
25
|
+
meta[k] = {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { meta, body };
|
|
30
|
+
}
|
|
31
|
+
function findSkillsDir() {
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const candidates = [
|
|
34
|
+
resolve(here, "..", "skills"),
|
|
35
|
+
resolve(here, "..", "..", "skills")
|
|
36
|
+
];
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
if (statSync(c).isDirectory()) return c;
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return candidates[0];
|
|
44
|
+
}
|
|
45
|
+
var cached = null;
|
|
46
|
+
async function loadSkills() {
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
const dir = findSkillsDir();
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await readdir(dir);
|
|
52
|
+
} catch {
|
|
53
|
+
cached = [];
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
const skills = [];
|
|
57
|
+
for (const name of entries) {
|
|
58
|
+
try {
|
|
59
|
+
const skillPath = join(dir, name, "SKILL.md");
|
|
60
|
+
const raw = await readFile(skillPath, "utf-8");
|
|
61
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
62
|
+
const skillName = typeof meta.name === "string" ? meta.name : name;
|
|
63
|
+
const description = typeof meta.description === "string" ? meta.description : "";
|
|
64
|
+
const version = typeof meta.version === "string" ? meta.version : "0.0.0";
|
|
65
|
+
skills.push({
|
|
66
|
+
name: skillName,
|
|
67
|
+
version,
|
|
68
|
+
description,
|
|
69
|
+
body: body.trimStart(),
|
|
70
|
+
content: raw,
|
|
71
|
+
supported: true
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
skills.sort((a, b) => {
|
|
77
|
+
if (a.name === "plaud-shared") return -1;
|
|
78
|
+
if (b.name === "plaud-shared") return 1;
|
|
79
|
+
return a.name.localeCompare(b.name);
|
|
80
|
+
});
|
|
81
|
+
cached = skills;
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
async function skillsCombined() {
|
|
85
|
+
const skills = await loadSkills();
|
|
86
|
+
return skills.map((s) => s.content).join("\n\n---\n\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
loadSkills,
|
|
91
|
+
skillsCombined
|
|
92
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlaudClient
|
|
3
|
+
} from "./chunk-SNSGVRCU.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
function buildExtraHeaders() {
|
|
7
|
+
const headers = {};
|
|
8
|
+
if (process.env.PLAUD_ENV) headers["x-pld-env"] = process.env.PLAUD_ENV;
|
|
9
|
+
if (process.env.PLAUD_REGION) headers["x-pld-region"] = process.env.PLAUD_REGION;
|
|
10
|
+
return headers;
|
|
11
|
+
}
|
|
12
|
+
var CONFIG = {
|
|
13
|
+
clientId: process.env.PLAUD_MCP_CLIENT_ID ?? process.env.PLAUD_CLIENT_ID ?? "client_9c501dad-8a0d-40b2-a7b0-d1cb8787f674",
|
|
14
|
+
clientSecret: process.env.PLAUD_CLIENT_SECRET ?? "",
|
|
15
|
+
redirectUri: "http://localhost:8199/auth/callback",
|
|
16
|
+
tokenFile: "tokens-mcp.json",
|
|
17
|
+
apiBase: process.env.PLAUD_API_BASE,
|
|
18
|
+
authorizationUrl: process.env.PLAUD_AUTH_URL,
|
|
19
|
+
tokenUrl: process.env.PLAUD_TOKEN_URL,
|
|
20
|
+
refreshUrl: process.env.PLAUD_REFRESH_URL,
|
|
21
|
+
extraHeaders: buildExtraHeaders()
|
|
22
|
+
};
|
|
23
|
+
var client = null;
|
|
24
|
+
function getClient() {
|
|
25
|
+
if (!client) {
|
|
26
|
+
client = new PlaudClient(CONFIG);
|
|
27
|
+
}
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
getClient
|
|
33
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
skillsCombined
|
|
3
|
+
} from "./chunk-4QBEOJPX.js";
|
|
4
|
+
|
|
5
|
+
// src/install-utils.ts
|
|
6
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { homedir, platform } from "os";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
var SKILLS_MARKER_START = "<!-- plaud-skills:start -->";
|
|
12
|
+
var SKILLS_MARKER_END = "<!-- plaud-skills:end -->";
|
|
13
|
+
function findNpxCommand() {
|
|
14
|
+
const npxName = platform() === "win32" ? "npx.cmd" : "npx";
|
|
15
|
+
const found = spawnSync(platform() === "win32" ? "where" : "which", [npxName], { encoding: "utf-8" });
|
|
16
|
+
const firstMatch = found.status === 0 ? found.stdout.split(/\r?\n/).find((line) => line.trim())?.trim() : void 0;
|
|
17
|
+
if (firstMatch) return firstMatch;
|
|
18
|
+
const sibling = join(dirname(process.execPath), npxName);
|
|
19
|
+
return existsSync(sibling) ? sibling : npxName;
|
|
20
|
+
}
|
|
21
|
+
function getMcpEntry() {
|
|
22
|
+
return {
|
|
23
|
+
command: findNpxCommand(),
|
|
24
|
+
args: ["-y", "@plaud-ai/mcp@latest"]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function copyToClipboard(content) {
|
|
28
|
+
if (platform() === "win32") {
|
|
29
|
+
return spawnSync("clip", [], { input: content }).status === 0;
|
|
30
|
+
}
|
|
31
|
+
if (platform() === "darwin") {
|
|
32
|
+
return spawnSync("pbcopy", [], { input: content }).status === 0;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
async function writeSkillsToClaudeCode() {
|
|
37
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
38
|
+
const combined = await skillsCombined();
|
|
39
|
+
const block = `${SKILLS_MARKER_START}
|
|
40
|
+
${combined}
|
|
41
|
+
${SKILLS_MARKER_END}`;
|
|
42
|
+
let existing = "";
|
|
43
|
+
try {
|
|
44
|
+
existing = await readFile(claudeMdPath, "utf-8");
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
if (existing.includes(SKILLS_MARKER_START)) {
|
|
48
|
+
const updated = existing.replace(
|
|
49
|
+
new RegExp(`${SKILLS_MARKER_START}[\\s\\S]*?${SKILLS_MARKER_END}`),
|
|
50
|
+
block
|
|
51
|
+
);
|
|
52
|
+
await mkdir(dirname(claudeMdPath), { recursive: true });
|
|
53
|
+
await writeFile(claudeMdPath, updated, "utf-8");
|
|
54
|
+
} else {
|
|
55
|
+
await mkdir(dirname(claudeMdPath), { recursive: true });
|
|
56
|
+
await writeFile(claudeMdPath, existing + (existing.endsWith("\n") ? "" : "\n") + block + "\n", "utf-8");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function removeSkillsFromClaudeCode() {
|
|
60
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
61
|
+
let existing = "";
|
|
62
|
+
try {
|
|
63
|
+
existing = await readFile(claudeMdPath, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!existing.includes(SKILLS_MARKER_START)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const updated = existing.replace(new RegExp(`\\n?${SKILLS_MARKER_START}[\\s\\S]*?${SKILLS_MARKER_END}\\n?`), "").trimEnd();
|
|
71
|
+
await writeFile(claudeMdPath, updated ? updated + "\n" : "", "utf-8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
getMcpEntry,
|
|
76
|
+
copyToClipboard,
|
|
77
|
+
writeSkillsToClaudeCode,
|
|
78
|
+
removeSkillsFromClaudeCode
|
|
79
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
var logger = pino(
|
|
4
|
+
{ level: process.env.LOG_LEVEL ?? "info" },
|
|
5
|
+
pino.destination(2)
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
// src/tools/index.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var MAX_FILTER_PAGES = 5;
|
|
11
|
+
var FILTER_PAGE_SIZE = 100;
|
|
12
|
+
function parseDate(s) {
|
|
13
|
+
if (!s) return null;
|
|
14
|
+
const d = new Date(s);
|
|
15
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
16
|
+
return d.getTime();
|
|
17
|
+
}
|
|
18
|
+
function registerTools(server, client) {
|
|
19
|
+
server.tool(
|
|
20
|
+
"list_files",
|
|
21
|
+
"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.",
|
|
22
|
+
{
|
|
23
|
+
page: z.number().optional().default(1).describe("Page number (ignored when filters are set)"),
|
|
24
|
+
page_size: z.number().optional().default(20).describe("Items per page (ignored when filters are set)"),
|
|
25
|
+
query: z.string().optional().describe("Case-insensitive substring match on recording name"),
|
|
26
|
+
date_from: z.string().optional().describe("Start date inclusive, YYYY-MM-DD"),
|
|
27
|
+
date_to: z.string().optional().describe("End date inclusive, YYYY-MM-DD")
|
|
28
|
+
},
|
|
29
|
+
async ({ page, page_size, query, date_from, date_to }) => {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const hasFilter = Boolean(query || date_from || date_to);
|
|
32
|
+
logger.info({ event: "tool_call", tool: "list_files", has_filter: hasFilter });
|
|
33
|
+
try {
|
|
34
|
+
if (!hasFilter) {
|
|
35
|
+
const result = await client.listFiles(page, page_size);
|
|
36
|
+
logger.info({ event: "tool_call_end", tool: "list_files", duration_ms: Date.now() - start });
|
|
37
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
38
|
+
}
|
|
39
|
+
const q = query?.toLowerCase();
|
|
40
|
+
const from = parseDate(date_from);
|
|
41
|
+
const toRaw = parseDate(date_to);
|
|
42
|
+
const to = toRaw !== null ? toRaw + 24 * 60 * 60 * 1e3 - 1 : null;
|
|
43
|
+
const matches = [];
|
|
44
|
+
let scanned = 0;
|
|
45
|
+
let truncated = false;
|
|
46
|
+
for (let p = 1; p <= MAX_FILTER_PAGES; p++) {
|
|
47
|
+
const pageResult = await client.listFiles(p, FILTER_PAGE_SIZE);
|
|
48
|
+
const items = pageResult.data;
|
|
49
|
+
scanned += items.length;
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
if (q && !(item.name ?? "").toLowerCase().includes(q)) continue;
|
|
52
|
+
if (from !== null || to !== null) {
|
|
53
|
+
const created = parseDate(item.created_at);
|
|
54
|
+
if (created === null) continue;
|
|
55
|
+
if (from !== null && created < from) continue;
|
|
56
|
+
if (to !== null && created > to) continue;
|
|
57
|
+
}
|
|
58
|
+
matches.push(item);
|
|
59
|
+
}
|
|
60
|
+
if (items.length < FILTER_PAGE_SIZE) break;
|
|
61
|
+
if (p === MAX_FILTER_PAGES) truncated = true;
|
|
62
|
+
}
|
|
63
|
+
logger.info({ event: "tool_call_end", tool: "list_files", duration_ms: Date.now() - start, scanned, matched: matches.length, truncated });
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
66
|
+
data: matches,
|
|
67
|
+
scanned,
|
|
68
|
+
matched: matches.length,
|
|
69
|
+
truncated,
|
|
70
|
+
note: truncated ? `Scanned first ${MAX_FILTER_PAGES * FILTER_PAGE_SIZE} recordings; narrow filters for a complete match.` : void 0
|
|
71
|
+
}, null, 2) }]
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.error({ event: "tool_call_error", tool: "list_files", duration_ms: Date.now() - start, error: String(err) });
|
|
75
|
+
return { content: [{ type: "text", text: `Failed to list files: ${err}` }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
server.tool(
|
|
80
|
+
"get_file",
|
|
81
|
+
"Get details of a specific Plaud recording by ID",
|
|
82
|
+
{ file_id: z.string().describe("The file ID to retrieve") },
|
|
83
|
+
async ({ file_id }) => {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
logger.info({ event: "tool_call", tool: "get_file", file_id });
|
|
86
|
+
try {
|
|
87
|
+
const file = await client.getFile(file_id);
|
|
88
|
+
logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start });
|
|
89
|
+
return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.error({ event: "tool_call_error", tool: "get_file", duration_ms: Date.now() - start, error: String(err) });
|
|
92
|
+
return { content: [{ type: "text", text: `Failed to get file: ${err}` }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
server.tool(
|
|
97
|
+
"get_note",
|
|
98
|
+
"Fetch AI-generated notes for a Plaud recording \u2014 compact summary, action items, and key topics",
|
|
99
|
+
{ file_id: z.string().describe("The file ID to retrieve notes for") },
|
|
100
|
+
async ({ file_id }) => {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
logger.info({ event: "tool_call", tool: "get_note", file_id });
|
|
103
|
+
try {
|
|
104
|
+
const file = await client.getFile(file_id);
|
|
105
|
+
logger.info({ event: "tool_call_end", tool: "get_note", duration_ms: Date.now() - start });
|
|
106
|
+
return { content: [{ type: "text", text: JSON.stringify(file.note_list ?? [], null, 2) }] };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.error({ event: "tool_call_error", tool: "get_note", duration_ms: Date.now() - start, error: String(err) });
|
|
109
|
+
return { content: [{ type: "text", text: `Failed to get note: ${err}` }], isError: true };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
server.tool(
|
|
114
|
+
"get_transcript",
|
|
115
|
+
"Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
|
|
116
|
+
{ file_id: z.string().describe("The file ID to retrieve transcript for") },
|
|
117
|
+
async ({ file_id }) => {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
logger.info({ event: "tool_call", tool: "get_transcript", file_id });
|
|
120
|
+
try {
|
|
121
|
+
const file = await client.getFile(file_id);
|
|
122
|
+
logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start });
|
|
123
|
+
return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error({ event: "tool_call_error", tool: "get_transcript", duration_ms: Date.now() - start, error: String(err) });
|
|
126
|
+
return { content: [{ type: "text", text: `Failed to get transcript: ${err}` }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
server.tool(
|
|
131
|
+
"get_current_user",
|
|
132
|
+
"Get current authenticated user info",
|
|
133
|
+
async () => {
|
|
134
|
+
const start = Date.now();
|
|
135
|
+
logger.info({ event: "tool_call", tool: "get_current_user" });
|
|
136
|
+
try {
|
|
137
|
+
const user = await client.getCurrentUser();
|
|
138
|
+
logger.info({ event: "tool_call_end", tool: "get_current_user", duration_ms: Date.now() - start });
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }] };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.error({ event: "tool_call_error", tool: "get_current_user", duration_ms: Date.now() - start, error: String(err) });
|
|
142
|
+
return { content: [{ type: "text", text: `Failed to get user info: ${err}` }], isError: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
logger,
|
|
150
|
+
registerTools
|
|
151
|
+
};
|
|
@@ -229,106 +229,6 @@ var PlaudClient = class {
|
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
-
// src/logger.ts
|
|
233
|
-
import pino from "pino";
|
|
234
|
-
var logger = pino({
|
|
235
|
-
level: process.env.LOG_LEVEL ?? "info"
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// src/tools/index.ts
|
|
239
|
-
import { z } from "zod";
|
|
240
|
-
function registerTools(server, client) {
|
|
241
|
-
server.tool(
|
|
242
|
-
"list_files",
|
|
243
|
-
"List Plaud recordings",
|
|
244
|
-
{
|
|
245
|
-
page: z.number().optional().default(1).describe("Page number"),
|
|
246
|
-
page_size: z.number().optional().default(20).describe("Items per page")
|
|
247
|
-
},
|
|
248
|
-
async ({ page, page_size }) => {
|
|
249
|
-
const start = Date.now();
|
|
250
|
-
logger.info({ event: "tool_call", tool: "list_files" });
|
|
251
|
-
try {
|
|
252
|
-
const result = await client.listFiles(page, page_size);
|
|
253
|
-
logger.info({ event: "tool_call_end", tool: "list_files", duration_ms: Date.now() - start });
|
|
254
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
255
|
-
} catch (err) {
|
|
256
|
-
logger.error({ event: "tool_call_error", tool: "list_files", duration_ms: Date.now() - start, error: String(err) });
|
|
257
|
-
return { content: [{ type: "text", text: `Failed to list files: ${err}` }], isError: true };
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
);
|
|
261
|
-
server.tool(
|
|
262
|
-
"get_file",
|
|
263
|
-
"Get details of a specific Plaud recording by ID",
|
|
264
|
-
{ file_id: z.string().describe("The file ID to retrieve") },
|
|
265
|
-
async ({ file_id }) => {
|
|
266
|
-
const start = Date.now();
|
|
267
|
-
logger.info({ event: "tool_call", tool: "get_file", file_id });
|
|
268
|
-
try {
|
|
269
|
-
const file = await client.getFile(file_id);
|
|
270
|
-
logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start });
|
|
271
|
-
return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
|
|
272
|
-
} catch (err) {
|
|
273
|
-
logger.error({ event: "tool_call_error", tool: "get_file", duration_ms: Date.now() - start, error: String(err) });
|
|
274
|
-
return { content: [{ type: "text", text: `Failed to get file: ${err}` }], isError: true };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
);
|
|
278
|
-
server.tool(
|
|
279
|
-
"get_note",
|
|
280
|
-
"Fetch AI-generated notes for a Plaud recording \u2014 compact summary, action items, and key topics",
|
|
281
|
-
{ file_id: z.string().describe("The file ID to retrieve notes for") },
|
|
282
|
-
async ({ file_id }) => {
|
|
283
|
-
const start = Date.now();
|
|
284
|
-
logger.info({ event: "tool_call", tool: "get_note", file_id });
|
|
285
|
-
try {
|
|
286
|
-
const file = await client.getFile(file_id);
|
|
287
|
-
logger.info({ event: "tool_call_end", tool: "get_note", duration_ms: Date.now() - start });
|
|
288
|
-
return { content: [{ type: "text", text: JSON.stringify(file.note_list ?? [], null, 2) }] };
|
|
289
|
-
} catch (err) {
|
|
290
|
-
logger.error({ event: "tool_call_error", tool: "get_note", duration_ms: Date.now() - start, error: String(err) });
|
|
291
|
-
return { content: [{ type: "text", text: `Failed to get note: ${err}` }], isError: true };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
);
|
|
295
|
-
server.tool(
|
|
296
|
-
"get_transcript",
|
|
297
|
-
"Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
|
|
298
|
-
{ file_id: z.string().describe("The file ID to retrieve transcript for") },
|
|
299
|
-
async ({ file_id }) => {
|
|
300
|
-
const start = Date.now();
|
|
301
|
-
logger.info({ event: "tool_call", tool: "get_transcript", file_id });
|
|
302
|
-
try {
|
|
303
|
-
const file = await client.getFile(file_id);
|
|
304
|
-
logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start });
|
|
305
|
-
return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
|
|
306
|
-
} catch (err) {
|
|
307
|
-
logger.error({ event: "tool_call_error", tool: "get_transcript", duration_ms: Date.now() - start, error: String(err) });
|
|
308
|
-
return { content: [{ type: "text", text: `Failed to get transcript: ${err}` }], isError: true };
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
);
|
|
312
|
-
server.tool(
|
|
313
|
-
"get_current_user",
|
|
314
|
-
"Get current authenticated user info",
|
|
315
|
-
async () => {
|
|
316
|
-
const start = Date.now();
|
|
317
|
-
logger.info({ event: "tool_call", tool: "get_current_user" });
|
|
318
|
-
try {
|
|
319
|
-
const user = await client.getCurrentUser();
|
|
320
|
-
logger.info({ event: "tool_call_end", tool: "get_current_user", duration_ms: Date.now() - start });
|
|
321
|
-
return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }] };
|
|
322
|
-
} catch (err) {
|
|
323
|
-
logger.error({ event: "tool_call_error", tool: "get_current_user", duration_ms: Date.now() - start, error: String(err) });
|
|
324
|
-
return { content: [{ type: "text", text: `Failed to get user info: ${err}` }], isError: true };
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
232
|
export {
|
|
331
|
-
PlaudClient
|
|
332
|
-
logger,
|
|
333
|
-
registerTools
|
|
233
|
+
PlaudClient
|
|
334
234
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,58 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
getClient
|
|
4
|
+
} from "./chunk-7KGB7GSZ.js";
|
|
5
|
+
import {
|
|
6
|
+
loadSkills
|
|
7
|
+
} from "./chunk-4QBEOJPX.js";
|
|
5
8
|
import {
|
|
6
|
-
PlaudClient,
|
|
7
9
|
registerTools
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-MPCF6HMK.js";
|
|
11
|
+
import "./chunk-SNSGVRCU.js";
|
|
9
12
|
|
|
10
13
|
// src/index.ts
|
|
11
14
|
import { createServer } from "http";
|
|
12
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
16
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
17
|
import open from "open";
|
|
15
|
-
|
|
16
|
-
// src/config.ts
|
|
17
|
-
function buildExtraHeaders() {
|
|
18
|
-
const headers = {};
|
|
19
|
-
if (process.env.PLAUD_ENV) headers["x-pld-env"] = process.env.PLAUD_ENV;
|
|
20
|
-
if (process.env.PLAUD_REGION) headers["x-pld-region"] = process.env.PLAUD_REGION;
|
|
21
|
-
return headers;
|
|
22
|
-
}
|
|
23
|
-
var CONFIG = {
|
|
24
|
-
clientId: process.env.PLAUD_MCP_CLIENT_ID ?? process.env.PLAUD_CLIENT_ID ?? "client_9c501dad-8a0d-40b2-a7b0-d1cb8787f674",
|
|
25
|
-
clientSecret: process.env.PLAUD_CLIENT_SECRET ?? "",
|
|
26
|
-
redirectUri: "http://localhost:8199/auth/callback",
|
|
27
|
-
tokenFile: "tokens-mcp.json",
|
|
28
|
-
apiBase: process.env.PLAUD_API_BASE,
|
|
29
|
-
authorizationUrl: process.env.PLAUD_AUTH_URL,
|
|
30
|
-
tokenUrl: process.env.PLAUD_TOKEN_URL,
|
|
31
|
-
refreshUrl: process.env.PLAUD_REFRESH_URL,
|
|
32
|
-
extraHeaders: buildExtraHeaders()
|
|
33
|
-
};
|
|
34
|
-
var client = null;
|
|
35
|
-
function getClient() {
|
|
36
|
-
if (!client) {
|
|
37
|
-
client = new PlaudClient(CONFIG);
|
|
38
|
-
}
|
|
39
|
-
return client;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/index.ts
|
|
43
18
|
var server = new McpServer({
|
|
44
19
|
name: "plaud",
|
|
45
|
-
version: "0.
|
|
20
|
+
version: "0.2.0"
|
|
46
21
|
});
|
|
47
22
|
var CALLBACK_PORT = 8199;
|
|
48
23
|
var LOGIN_TIMEOUT_MS = 12e4;
|
|
49
24
|
server.tool("login", "Log in, sign in, or authenticate with Plaud account via OAuth (opens browser)", async () => {
|
|
50
|
-
const
|
|
51
|
-
const existingToken = await
|
|
25
|
+
const client = getClient();
|
|
26
|
+
const existingToken = await client.auth.getAccessToken();
|
|
52
27
|
if (existingToken) {
|
|
53
28
|
return { content: [{ type: "text", text: "Already logged in." }] };
|
|
54
29
|
}
|
|
55
|
-
const { url, codeVerifier, state } =
|
|
30
|
+
const { url, codeVerifier, state } = client.auth.createAuthorizationRequest();
|
|
56
31
|
return new Promise((resolve) => {
|
|
57
32
|
const httpServer = createServer(async (req, res) => {
|
|
58
33
|
const reqUrl = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
@@ -73,7 +48,7 @@ server.tool("login", "Log in, sign in, or authenticate with Plaud account via OA
|
|
|
73
48
|
return;
|
|
74
49
|
}
|
|
75
50
|
try {
|
|
76
|
-
await
|
|
51
|
+
await client.auth.exchangeCode(code, codeVerifier, state);
|
|
77
52
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
78
53
|
res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
|
|
79
54
|
cleanup();
|
|
@@ -106,10 +81,14 @@ server.tool("login", "Log in, sign in, or authenticate with Plaud account via OA
|
|
|
106
81
|
}, LOGIN_TIMEOUT_MS);
|
|
107
82
|
httpServer.listen(CALLBACK_PORT, () => {
|
|
108
83
|
open(url).catch(() => {
|
|
109
|
-
cleanup();
|
|
110
84
|
resolve({
|
|
111
|
-
content: [{
|
|
112
|
-
|
|
85
|
+
content: [{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Could not open browser automatically. Open this URL within 2 minutes to authenticate:
|
|
88
|
+
${url}
|
|
89
|
+
|
|
90
|
+
If this is a remote/headless machine, forward local port 8199 to this machine first, then open the URL locally.`
|
|
91
|
+
}],
|
|
113
92
|
isError: true
|
|
114
93
|
});
|
|
115
94
|
});
|
|
@@ -125,56 +104,87 @@ ${url}` }],
|
|
|
125
104
|
});
|
|
126
105
|
registerTools(server, getClient());
|
|
127
106
|
server.tool("logout", "Log out, sign out, revoke authorization, and disconnect from Plaud account", async () => {
|
|
128
|
-
const
|
|
129
|
-
const existingToken = await
|
|
107
|
+
const client = getClient();
|
|
108
|
+
const existingToken = await client.auth.getAccessToken();
|
|
130
109
|
if (!existingToken) {
|
|
131
110
|
return { content: [{ type: "text", text: "Already logged out." }] };
|
|
132
111
|
}
|
|
133
112
|
try {
|
|
134
|
-
await
|
|
113
|
+
await client.revokeCurrentUser();
|
|
135
114
|
} catch {
|
|
136
115
|
}
|
|
137
|
-
await
|
|
116
|
+
await client.auth.logout();
|
|
138
117
|
return {
|
|
139
118
|
content: [{ type: "text", text: "Logged out and revoked authorization." }]
|
|
140
119
|
};
|
|
141
120
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
121
|
+
async function registerSkillPrompts() {
|
|
122
|
+
const skills = await loadSkills();
|
|
123
|
+
for (const skill of skills) {
|
|
124
|
+
server.prompt(skill.name, skill.description, () => ({
|
|
125
|
+
messages: [{ role: "user", content: { type: "text", text: skill.content } }]
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
146
128
|
}
|
|
147
129
|
async function main() {
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
const sub = process.argv[2];
|
|
131
|
+
const sub2 = process.argv[3];
|
|
132
|
+
if (sub === "install") {
|
|
133
|
+
const { runInstall } = await import("./install-I2N3DJG4.js");
|
|
134
|
+
const args = process.argv.slice(3);
|
|
135
|
+
const yes = args.some((a) => a === "--yes" || a === "-y");
|
|
136
|
+
const noLogin = args.some((a) => a === "--no-login");
|
|
137
|
+
await runInstall({ yes, noLogin });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (sub === "clean-plugin") {
|
|
141
|
+
const { runCleanPlugin } = await import("./setup-QRUDA7ES.js");
|
|
150
142
|
await runCleanPlugin();
|
|
151
143
|
return;
|
|
152
144
|
}
|
|
153
|
-
if (
|
|
154
|
-
const { runSetupCodex } = await import("./setup-
|
|
145
|
+
if (sub === "setup" && sub2 === "codex") {
|
|
146
|
+
const { runSetupCodex } = await import("./setup-QRUDA7ES.js");
|
|
155
147
|
await runSetupCodex();
|
|
156
148
|
return;
|
|
157
149
|
}
|
|
158
|
-
if (
|
|
159
|
-
const { runUnsetupCodex } = await import("./setup-
|
|
150
|
+
if (sub === "unsetup" && sub2 === "codex") {
|
|
151
|
+
const { runUnsetupCodex } = await import("./setup-QRUDA7ES.js");
|
|
160
152
|
await runUnsetupCodex();
|
|
161
153
|
return;
|
|
162
154
|
}
|
|
163
|
-
if (
|
|
164
|
-
const { runSetup } = await import("./setup-
|
|
155
|
+
if (sub === "setup") {
|
|
156
|
+
const { runSetup } = await import("./setup-QRUDA7ES.js");
|
|
165
157
|
await runSetup();
|
|
166
158
|
return;
|
|
167
159
|
}
|
|
168
|
-
if (
|
|
169
|
-
const { runUnsetup } = await import("./setup-
|
|
160
|
+
if (sub === "unsetup") {
|
|
161
|
+
const { runUnsetup } = await import("./setup-QRUDA7ES.js");
|
|
170
162
|
await runUnsetup();
|
|
171
163
|
return;
|
|
172
164
|
}
|
|
173
|
-
if (
|
|
174
|
-
const { startHttpServer } = await import("./server-
|
|
165
|
+
if (sub === "http") {
|
|
166
|
+
const { startHttpServer } = await import("./server-D3H6AMVD.js");
|
|
175
167
|
startHttpServer();
|
|
176
168
|
return;
|
|
177
169
|
}
|
|
170
|
+
if (sub === "--help" || sub === "-h") {
|
|
171
|
+
console.log(`plaud-mcp \u2014 Plaud MCP server
|
|
172
|
+
|
|
173
|
+
Usage:
|
|
174
|
+
plaud-mcp start MCP over stdio
|
|
175
|
+
plaud-mcp install detect AI clients, configure them, and run OAuth inline (recommended)
|
|
176
|
+
plaud-mcp install --yes non-interactive; auto-configure every detected client (for agent-driven install)
|
|
177
|
+
plaud-mcp install --no-login skip the OAuth step (for CI / headless setup)
|
|
178
|
+
plaud-mcp http start HTTP MCP server (advanced)
|
|
179
|
+
plaud-mcp setup [deprecated] alias for install (Claude Desktop only)
|
|
180
|
+
plaud-mcp setup codex [deprecated] alias for install (Codex only)
|
|
181
|
+
plaud-mcp unsetup remove Plaud from Claude Desktop
|
|
182
|
+
plaud-mcp unsetup codex remove Plaud from Codex Desktop
|
|
183
|
+
plaud-mcp clean-plugin clear Claude Code plugin cache
|
|
184
|
+
`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await registerSkillPrompts();
|
|
178
188
|
const transport = new StdioServerTransport();
|
|
179
189
|
await server.connect(transport);
|
|
180
190
|
}
|