@plaud-ai/mcp 0.1.32 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,304 @@
1
+ import {
2
+ PlaudClient,
3
+ logger,
4
+ registerTools
5
+ } from "./chunk-YMQLKZLW.js";
6
+
7
+ // src/http/server.ts
8
+ import express from "express";
9
+ import { createServer } from "http";
10
+ import { randomUUID as randomUUID2 } from "crypto";
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13
+ import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
14
+ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
15
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
16
+
17
+ // src/http/oauth-provider.ts
18
+ import { randomUUID } from "crypto";
19
+ import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
20
+ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
21
+ _plaudClientId;
22
+ _plaudClientSecret;
23
+ _plaudTokenUrl;
24
+ _plaudApiBase;
25
+ _callbackUrl;
26
+ _registeredClients = /* @__PURE__ */ new Map();
27
+ // internalState → { clientRedirectUri, originalState }
28
+ // We generate our own state to track the pending flow regardless of whether the client sent one.
29
+ _pendingStates = /* @__PURE__ */ new Map();
30
+ // code → internalState: lets exchangeAuthorizationCode include state in the Plaud token request
31
+ _pendingCodes = /* @__PURE__ */ new Map();
32
+ constructor(options) {
33
+ const authUrl = options.authUrl ?? "https://web.plaud.ai/platform/oauth";
34
+ const tokenUrl = options.tokenUrl ?? "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
35
+ const apiBase = options.apiBase ?? "https://platform.plaud.ai/developer/api";
36
+ super({
37
+ endpoints: {
38
+ authorizationUrl: authUrl,
39
+ tokenUrl
40
+ },
41
+ verifyAccessToken: async (token) => {
42
+ const client = new PlaudClient({
43
+ clientId: options.clientId,
44
+ clientSecret: options.clientSecret,
45
+ redirectUri: "",
46
+ apiBase,
47
+ staticToken: token
48
+ });
49
+ try {
50
+ const user = await client.getCurrentUser();
51
+ let expiresAt;
52
+ try {
53
+ const payload = JSON.parse(
54
+ Buffer.from(token.split(".")[1], "base64url").toString()
55
+ );
56
+ expiresAt = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + 3600;
57
+ } catch {
58
+ expiresAt = Math.floor(Date.now() / 1e3) + 3600;
59
+ }
60
+ const authInfo = {
61
+ token,
62
+ clientId: String(user.id ?? "unknown"),
63
+ scopes: [],
64
+ expiresAt
65
+ };
66
+ logger.info({ event: "token_verified", client_id: authInfo.clientId, expires_at: expiresAt });
67
+ return authInfo;
68
+ } catch (err) {
69
+ logger.warn({ event: "token_verify_failed", error: String(err) });
70
+ throw new Error("Invalid or expired token");
71
+ }
72
+ },
73
+ getClient: async (id) => this._registeredClients.get(id)
74
+ });
75
+ this._plaudClientId = options.clientId;
76
+ this._plaudClientSecret = options.clientSecret;
77
+ this._plaudTokenUrl = tokenUrl;
78
+ this._plaudApiBase = apiBase;
79
+ this._callbackUrl = options.callbackUrl;
80
+ this.skipLocalPkceValidation = true;
81
+ }
82
+ // Override clientsStore to add in-memory dynamic client registration
83
+ get clientsStore() {
84
+ return {
85
+ getClient: async (id) => this._registeredClients.get(id),
86
+ registerClient: async (client) => {
87
+ const full = {
88
+ ...client,
89
+ client_id: randomUUID(),
90
+ client_id_issued_at: Math.floor(Date.now() / 1e3)
91
+ };
92
+ this._registeredClients.set(full.client_id, full);
93
+ return full;
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Redirect to Plaud using our own registered callback URL.
99
+ * Store the client's original redirect_uri keyed by state so we can forward after Plaud calls back.
100
+ */
101
+ async authorize(_client, params, res) {
102
+ const internalState = randomUUID();
103
+ this._pendingStates.set(internalState, {
104
+ clientRedirectUri: params.redirectUri,
105
+ originalState: params.state
106
+ });
107
+ logger.info({ event: "oauth_authorize_start", internal_state: internalState, redirect_uri: params.redirectUri });
108
+ const targetUrl = new URL(this._endpoints.authorizationUrl);
109
+ const searchParams = new URLSearchParams({
110
+ client_id: this._plaudClientId,
111
+ response_type: "code",
112
+ redirect_uri: this._callbackUrl,
113
+ code_challenge: params.codeChallenge,
114
+ code_challenge_method: "S256",
115
+ state: internalState
116
+ // always send our internal state to Plaud
117
+ });
118
+ if (params.scopes?.length) searchParams.set("scope", params.scopes.join(" "));
119
+ targetUrl.search = searchParams.toString();
120
+ res.redirect(targetUrl.toString());
121
+ }
122
+ /**
123
+ * Called when Plaud redirects to our /oauth/callback.
124
+ * Looks up the original client redirect_uri and forwards the code+state to it.
125
+ */
126
+ handleCallback(code, state, res) {
127
+ const pending = this._pendingStates.get(state);
128
+ if (!pending) {
129
+ logger.warn({ event: "oauth_callback_unknown_state", state });
130
+ res.status(400).send("Unknown state \u2014 authorization request not found");
131
+ return;
132
+ }
133
+ this._pendingStates.delete(state);
134
+ this._pendingCodes.set(code, state);
135
+ logger.info({ event: "oauth_callback_received", internal_state: state });
136
+ const target = new URL(pending.clientRedirectUri);
137
+ target.searchParams.set("code", code);
138
+ if (pending.originalState) {
139
+ target.searchParams.set("state", pending.originalState);
140
+ }
141
+ res.redirect(target.toString());
142
+ }
143
+ // Override to use Plaud's Basic auth + our fixed callback URL for redirect_uri
144
+ async exchangeAuthorizationCode(_client, authorizationCode, codeVerifier, _redirectUri) {
145
+ const basicAuth = Buffer.from(
146
+ `${this._plaudClientId}:${this._plaudClientSecret}`
147
+ ).toString("base64");
148
+ const internalState = this._pendingCodes.get(authorizationCode);
149
+ this._pendingCodes.delete(authorizationCode);
150
+ const body = {
151
+ grant_type: "authorization_code",
152
+ code: authorizationCode,
153
+ redirect_uri: this._callbackUrl
154
+ };
155
+ if (codeVerifier) body.code_verifier = codeVerifier;
156
+ if (internalState) body.state = internalState;
157
+ const fetchRes = await fetch(this._plaudTokenUrl, {
158
+ method: "POST",
159
+ headers: {
160
+ "Content-Type": "application/x-www-form-urlencoded",
161
+ Accept: "application/json",
162
+ Authorization: `Basic ${basicAuth}`
163
+ },
164
+ body: new URLSearchParams(body)
165
+ });
166
+ if (!fetchRes.ok) {
167
+ const text = await fetchRes.text();
168
+ logger.error({ event: "oauth_token_exchange_failed", status: fetchRes.status, body: text });
169
+ throw new Error(`Token exchange failed: ${fetchRes.status} ${text}`);
170
+ }
171
+ const data = await fetchRes.json();
172
+ logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
173
+ return {
174
+ access_token: data.access_token,
175
+ token_type: data.token_type ?? "Bearer",
176
+ refresh_token: data.refresh_token,
177
+ expires_in: data.expires_in
178
+ };
179
+ }
180
+ };
181
+
182
+ // src/http/server.ts
183
+ var HTTP_PORT = Number(process.env.PLAUD_HTTP_PORT ?? 3e3);
184
+ var HTTP_HOST = process.env.PLAUD_HTTP_HOST ?? "0.0.0.0";
185
+ var CALLBACK_PORT = 8199;
186
+ var CALLBACK_PATH = "/auth/callback";
187
+ var CALLBACK_URL = process.env.PLAUD_CALLBACK_URL ?? `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
188
+ function startHttpServer() {
189
+ const clientId = process.env.PLAUD_MCP_CLIENT_ID ?? process.env.PLAUD_CLIENT_ID ?? "client_9c501dad-8a0d-40b2-a7b0-d1cb8787f674";
190
+ const clientSecret = process.env.PLAUD_CLIENT_SECRET ?? "";
191
+ const apiBase = process.env.PLAUD_API_BASE;
192
+ const serverUrl = process.env.PLAUD_SERVER_URL ?? `http://localhost:${HTTP_PORT}`;
193
+ const provider = new PlaudOAuthProvider({
194
+ clientId,
195
+ clientSecret,
196
+ callbackUrl: CALLBACK_URL,
197
+ authUrl: process.env.PLAUD_AUTH_URL,
198
+ tokenUrl: process.env.PLAUD_TOKEN_URL,
199
+ apiBase
200
+ });
201
+ const issuerUrl = new URL(serverUrl);
202
+ const app = createMcpExpressApp({ host: HTTP_HOST });
203
+ app.get("/health", (_req, res) => {
204
+ res.json({ status: "ok", uptime_s: Math.floor(process.uptime()) });
205
+ });
206
+ if (process.env.PLAUD_CALLBACK_URL) {
207
+ app.get(CALLBACK_PATH, (req, res) => {
208
+ const code = req.query["code"];
209
+ const state = req.query["state"];
210
+ if (!code || !state) {
211
+ logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
212
+ res.status(400).send("Missing code or state");
213
+ return;
214
+ }
215
+ provider.handleCallback(code, state, res);
216
+ });
217
+ }
218
+ app.use((req, res, next) => {
219
+ const reqId = req.headers["x-request-id"] ?? randomUUID2();
220
+ res.locals["reqId"] = reqId;
221
+ logger.info({ event: "http_request", req_id: reqId, method: req.method, path: req.url, user_agent: req.headers["user-agent"] ?? null });
222
+ next();
223
+ });
224
+ app.post("/token", express.urlencoded({ extended: false }), (req, _res, next) => {
225
+ if (req.body?.resource && !URL.canParse(req.body.resource)) {
226
+ delete req.body.resource;
227
+ }
228
+ next();
229
+ });
230
+ app.use(
231
+ mcpAuthRouter({
232
+ provider,
233
+ issuerUrl,
234
+ baseUrl: issuerUrl,
235
+ resourceName: "Plaud MCP Server"
236
+ })
237
+ );
238
+ app.post(
239
+ "/mcp",
240
+ requireBearerAuth({ verifier: provider, resourceMetadataUrl: `${serverUrl}/.well-known/oauth-protected-resource` }),
241
+ async (req, res) => {
242
+ const token = req.auth.token;
243
+ const reqId = res.locals["reqId"] ?? randomUUID2();
244
+ const reqLog = logger.child({ req_id: reqId });
245
+ const startMs = Date.now();
246
+ reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
247
+ const client = new PlaudClient({
248
+ clientId,
249
+ clientSecret,
250
+ redirectUri: "",
251
+ apiBase,
252
+ staticToken: token
253
+ });
254
+ const mcpServer = new McpServer({ name: "plaud", version: "0.1.0" });
255
+ registerTools(mcpServer, client);
256
+ const transport = new StreamableHTTPServerTransport({
257
+ sessionIdGenerator: void 0
258
+ // stateless
259
+ });
260
+ try {
261
+ await mcpServer.connect(transport);
262
+ await transport.handleRequest(req, res, req.body);
263
+ const clientVersion = mcpServer.server.getClientVersion();
264
+ if (clientVersion) {
265
+ reqLog.info({ event: "mcp_client_info", client_name: clientVersion.name, client_version: clientVersion.version });
266
+ }
267
+ reqLog.info({ event: "mcp_request_end", duration_ms: Date.now() - startMs });
268
+ } catch (err) {
269
+ reqLog.error({ event: "mcp_request_error", error: String(err), duration_ms: Date.now() - startMs });
270
+ if (!res.headersSent) {
271
+ res.status(500).json({ error: String(err) });
272
+ }
273
+ } finally {
274
+ await mcpServer.close();
275
+ }
276
+ }
277
+ );
278
+ const callbackApp = express();
279
+ callbackApp.get(CALLBACK_PATH, (req, res) => {
280
+ const code = req.query["code"];
281
+ const state = req.query["state"];
282
+ if (!code || !state) {
283
+ logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
284
+ res.status(400).send("Missing code or state");
285
+ return;
286
+ }
287
+ provider.handleCallback(code, state, res);
288
+ });
289
+ createServer(callbackApp).listen(CALLBACK_PORT, "localhost", () => {
290
+ logger.info({ event: "server_start", role: "oauth_callback", url: CALLBACK_URL });
291
+ });
292
+ app.listen(HTTP_PORT, HTTP_HOST, () => {
293
+ logger.info({
294
+ event: "server_start",
295
+ role: "mcp_http",
296
+ url: serverUrl,
297
+ mcp_endpoint: `${serverUrl}/mcp`,
298
+ oauth_metadata: `${serverUrl}/.well-known/oauth-authorization-server`
299
+ });
300
+ });
301
+ }
302
+ export {
303
+ startHttpServer
304
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SKILLS_COMBINED
3
- } from "./chunk-5JTZ7NKP.js";
3
+ } from "./chunk-7PYQFJJW.js";
4
4
 
5
5
  // src/setup.ts
6
6
  import { readFile, writeFile, mkdir, rm } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/mcp",
3
- "version": "0.1.32",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,11 +21,14 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.12.0",
24
+ "express": "^5.2.1",
24
25
  "open": "^10.2.0",
26
+ "pino": "^10.3.1",
25
27
  "zod": "^4.3.6"
26
28
  },
27
29
  "devDependencies": {
28
30
  "@plaud-ai/shared": "workspace:*",
31
+ "@types/express": "^5.0.6",
29
32
  "@types/node": "^25.5.0",
30
33
  "typescript": "^5.7.0"
31
34
  }
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plaud",
3
- "version": "0.1.32",
3
+ "version": "0.1.58",
4
4
  "description": "Access your Plaud recordings in Claude",
5
5
  "author": {
6
6
  "name": "Plaud AI"