minutes-mcp 0.5.0 → 0.5.2

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +240 -11
  3. package/package.json +5 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mat Silverstein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import { readFile } from "fs/promises";
31
31
  import { dirname, extname, join, resolve } from "path";
32
32
  import { fileURLToPath } from "url";
33
33
  import { homedir } from "os";
34
+ import * as reader from "minutes-sdk";
34
35
  const UI_RESOURCE_URI = "ui://minutes/dashboard";
35
36
  const execFileAsync = promisify(execFile);
36
37
  // ── QMD semantic search (optional — falls back to CLI) ──────
@@ -59,16 +60,14 @@ async function isQmdAvailable() {
59
60
  }
60
61
  async function enrichWithFrontmatter(qmdResults) {
61
62
  return Promise.all(qmdResults.map(async (r) => {
63
+ const filePath = r.source_path || r.path;
62
64
  try {
63
- const head = (await readFile(r.source_path || r.path, "utf-8")).slice(0, 600);
64
- const title = head.match(/^title:\s*(.+)$/m)?.[1]?.trim() || "";
65
- const date = head.match(/^date:\s*(.+)$/m)?.[1]?.trim() || "";
66
- const contentType = head.match(/^type:\s*(.+)$/m)?.[1]?.trim() || "meeting";
65
+ const meeting = await reader.getMeeting(filePath);
67
66
  return {
68
- date,
69
- title,
70
- content_type: contentType,
71
- path: r.source_path || r.path,
67
+ date: meeting?.frontmatter.date || "",
68
+ title: meeting?.frontmatter.title || "",
69
+ content_type: meeting?.frontmatter.type || "meeting",
70
+ path: filePath,
72
71
  snippet: r.snippet || "",
73
72
  };
74
73
  }
@@ -77,7 +76,7 @@ async function enrichWithFrontmatter(qmdResults) {
77
76
  date: "",
78
77
  title: "",
79
78
  content_type: "meeting",
80
- path: r.source_path || r.path,
79
+ path: filePath,
81
80
  snippet: r.snippet || "",
82
81
  };
83
82
  }
@@ -141,6 +140,36 @@ function findMinutesBinary() {
141
140
  return "minutes";
142
141
  }
143
142
  const MINUTES_BIN = findMinutesBinary();
143
+ // ── CLI availability detection ──────────────────────────────
144
+ // When installed via `npx minutes-mcp`, the Rust CLI may not be present.
145
+ // In that case, read-only tools use the pure-TS reader module.
146
+ let cliAvailable = null;
147
+ let cliCheckedAt = 0;
148
+ const CLI_CACHE_TTL_MS = 5 * 60 * 1000; // re-check every 5 minutes
149
+ async function isCliAvailable() {
150
+ // Cache hit: return true permanently (CLI won't disappear mid-session)
151
+ // Cache miss (false): re-probe after TTL so installing CLI mid-session works
152
+ if (cliAvailable === true)
153
+ return true;
154
+ if (cliAvailable === false && Date.now() - cliCheckedAt < CLI_CACHE_TTL_MS)
155
+ return false;
156
+ try {
157
+ await execFileAsync(MINUTES_BIN, ["--version"], { timeout: 5000 });
158
+ cliAvailable = true;
159
+ cliCheckedAt = Date.now();
160
+ console.error("[Minutes] CLI found — full mode (all tools enabled)");
161
+ }
162
+ catch {
163
+ cliAvailable = false;
164
+ cliCheckedAt = Date.now();
165
+ console.error("[Minutes] CLI not found — read-only mode. Install for recording: brew install minutes");
166
+ }
167
+ return cliAvailable;
168
+ }
169
+ const CLI_INSTALL_MSG = "Recording requires the minutes CLI binary. Install it:\n" +
170
+ " macOS: brew tap silverstein/tap && brew install minutes\n" +
171
+ " Any: cargo install minutes-cli\n" +
172
+ " Source: https://github.com/silverstein/minutes";
144
173
  // ── Helper: run minutes CLI command (uses execFile, not exec) ──
145
174
  async function runMinutes(args, timeoutMs = 30000) {
146
175
  try {
@@ -208,7 +237,7 @@ function validatePathInDirectories(path, roots, allowedExts) {
208
237
  // ── MCP Server ──────────────────────────────────────────────
209
238
  const server = new McpServer({
210
239
  name: "minutes",
211
- version: "0.3.0",
240
+ version: "0.5.2",
212
241
  });
213
242
  // Configurable directories — override via env vars in Claude Desktop extension settings
214
243
  const MEETINGS_DIR = process.env.MEETINGS_DIR || join(homedir(), "meetings");
@@ -234,6 +263,9 @@ server.tool("start_recording", "Start recording audio from the default input dev
234
263
  .default("meeting")
235
264
  .describe("Live capture mode"),
236
265
  }, { title: "Start Recording", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ title, mode }) => {
266
+ if (!(await isCliAvailable())) {
267
+ return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
268
+ }
237
269
  const { stdout: statusOut } = await runMinutes(["status"]);
238
270
  const status = parseJsonOutput(statusOut);
239
271
  if (status.recording) {
@@ -274,6 +306,9 @@ server.tool("start_recording", "Start recording audio from the default input dev
274
306
  });
275
307
  // ── Tool: stop_recording ────────────────────────────────────
276
308
  server.tool("stop_recording", "Stop the current recording and process it (transcribe, diarize, summarize).", {}, { title: "Stop Recording", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async () => {
309
+ if (!(await isCliAvailable())) {
310
+ return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
311
+ }
277
312
  try {
278
313
  const { stdout, stderr } = await runMinutes(["stop"], 180000);
279
314
  const result = parseJsonOutput(stdout);
@@ -293,6 +328,9 @@ server.tool("stop_recording", "Stop the current recording and process it (transc
293
328
  });
294
329
  // ── Tool: get_status ────────────────────────────────────────
295
330
  server.tool("get_status", "Check if a recording is currently in progress.", {}, { title: "Recording Status", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async () => {
331
+ if (!(await isCliAvailable())) {
332
+ return { content: [{ type: "text", text: `No recording in progress (read-only mode).\n\n${CLI_INSTALL_MSG}` }] };
333
+ }
296
334
  const { stdout } = await runMinutes(["status"]);
297
335
  const status = parseJsonOutput(stdout);
298
336
  const modeLabel = status.recording_mode === "quick-thought" ? "Quick thought" : "Recording";
@@ -314,6 +352,36 @@ registerAppTool(server, "list_meetings", {
314
352
  annotations: { title: "List Meetings", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
315
353
  _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
316
354
  }, async ({ limit, type: contentType }) => {
355
+ // Pure-TS fallback when CLI is not available
356
+ if (!(await isCliAvailable())) {
357
+ const meetings = await reader.listMeetings(MEETINGS_DIR, limit);
358
+ const filtered = contentType
359
+ ? meetings.filter((m) => m.frontmatter.type === contentType)
360
+ : meetings;
361
+ const openActions = await reader.findOpenActions(MEETINGS_DIR);
362
+ if (filtered.length === 0) {
363
+ return {
364
+ content: [{ type: "text", text: "No meetings or memos found." }],
365
+ structuredContent: { meetings: [], actions: [], view: "dashboard" },
366
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "dashboard" },
367
+ };
368
+ }
369
+ const text = filtered
370
+ .map((m) => `${m.frontmatter.date} — ${m.frontmatter.title} [${m.frontmatter.type}]\n ${m.path}`)
371
+ .join("\n\n");
372
+ const meetingsJson = filtered.map((m) => ({
373
+ date: m.frontmatter.date,
374
+ title: m.frontmatter.title,
375
+ content_type: m.frontmatter.type,
376
+ path: m.path,
377
+ duration: m.frontmatter.duration,
378
+ }));
379
+ return {
380
+ content: [{ type: "text", text }],
381
+ structuredContent: { meetings: meetingsJson, actions: openActions.map((a) => a.item), view: "dashboard" },
382
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "dashboard" },
383
+ };
384
+ }
317
385
  const args = ["list", "--limit", String(limit)];
318
386
  if (contentType)
319
387
  args.push("-t", contentType);
@@ -367,6 +435,40 @@ registerAppTool(server, "search_meetings", {
367
435
  annotations: { title: "Search Meetings", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
368
436
  _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
369
437
  }, async ({ query, type: contentType, since, limit, intent_kind, owner, intents_only }) => {
438
+ // Pure-TS fallback when CLI is not available
439
+ if (!(await isCliAvailable())) {
440
+ const droppedFilters = [since && "since", intent_kind && "intent_kind", owner && "owner", intents_only && "intents_only"].filter(Boolean);
441
+ const filterWarning = droppedFilters.length > 0
442
+ ? `\n\n(Note: ${droppedFilters.join(", ")} filters require the CLI. Install: brew install minutes)`
443
+ : "";
444
+ const results = await reader.searchMeetings(MEETINGS_DIR, query, limit);
445
+ const filtered = contentType
446
+ ? results.filter((m) => m.frontmatter.type === contentType)
447
+ : results;
448
+ if (filtered.length === 0) {
449
+ return {
450
+ content: [{ type: "text", text: `No results for "${query}".${filterWarning}` }],
451
+ structuredContent: { results: [], view: "search" },
452
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "search" },
453
+ };
454
+ }
455
+ const text = filtered
456
+ .map((m) => `${m.frontmatter.date} — ${m.frontmatter.title} [${m.frontmatter.type}]\n ${m.path}`)
457
+ .join("\n\n") + filterWarning;
458
+ return {
459
+ content: [{ type: "text", text }],
460
+ structuredContent: {
461
+ results: filtered.map((m) => ({
462
+ date: m.frontmatter.date,
463
+ title: m.frontmatter.title,
464
+ content_type: m.frontmatter.type,
465
+ path: m.path,
466
+ })),
467
+ view: "search",
468
+ },
469
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "search" },
470
+ };
471
+ }
370
472
  // Intent/metadata queries always use CLI (QMD doesn't index YAML frontmatter fields)
371
473
  const useCliOnly = intents_only || intent_kind || owner || since;
372
474
  // Try QMD semantic search for text queries
@@ -436,6 +538,9 @@ registerAppTool(server, "consistency_report", {
436
538
  annotations: { title: "Consistency Report", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
437
539
  _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
438
540
  }, async ({ owner, stale_after_days }) => {
541
+ if (!(await isCliAvailable())) {
542
+ return { content: [{ type: "text", text: `Consistency reports require the full CLI for structured intent analysis.\n\n${CLI_INSTALL_MSG}` }] };
543
+ }
439
544
  const args = ["consistency", "--stale-after-days", String(stale_after_days)];
440
545
  if (owner)
441
546
  args.push("--owner", owner);
@@ -485,6 +590,25 @@ registerAppTool(server, "get_person_profile", {
485
590
  annotations: { title: "Person Profile", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
486
591
  _meta: { ui: { resourceUri: UI_RESOURCE_URI } },
487
592
  }, async ({ name }) => {
593
+ // Pure-TS fallback when CLI is not available
594
+ if (!(await isCliAvailable())) {
595
+ const profile = await reader.getPersonProfile(MEETINGS_DIR, name);
596
+ const sections = [];
597
+ if (profile.topics.length > 0)
598
+ sections.push("Topics: " + profile.topics.join(", "));
599
+ if (profile.meetings.length > 0) {
600
+ sections.push("Meetings:\n" + profile.meetings.map((m) => `- ${m.date} — ${m.title}`).join("\n"));
601
+ }
602
+ if (profile.openActions.length > 0) {
603
+ sections.push("Open actions:\n" + profile.openActions.map((a) => `- ${a.task} (${a.status})`).join("\n"));
604
+ }
605
+ const text = sections.length > 0 ? sections.join("\n\n") : `No profile data found for ${name}.`;
606
+ return {
607
+ content: [{ type: "text", text }],
608
+ structuredContent: { name, top_topics: profile.topics.map((t) => ({ topic: t, count: 1 })), open_intents: profile.openActions, recent_meetings: profile.meetings, view: "person" },
609
+ _meta: { ui: { resourceUri: UI_RESOURCE_URI }, view: "person" },
610
+ };
611
+ }
488
612
  const { stdout, stderr } = await runMinutes(["person", name]);
489
613
  const profile = parseJsonOutput(stdout);
490
614
  if (!profile || typeof profile !== "object") {
@@ -537,6 +661,15 @@ server.tool("research_topic", "Research a topic across meetings, decisions, and
537
661
  since: z.string().optional().describe("Only results after this date (ISO)"),
538
662
  attendee: z.string().optional().describe("Filter by attendee / person"),
539
663
  }, { title: "Research Topic", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ query, type: contentType, since, attendee }) => {
664
+ if (!(await isCliAvailable())) {
665
+ // Fallback: basic search when CLI is not available
666
+ const results = await reader.searchMeetings(MEETINGS_DIR, query, 20);
667
+ const filtered = contentType ? results.filter((m) => m.frontmatter.type === contentType) : results;
668
+ const text = filtered.length > 0
669
+ ? filtered.map((m) => `${m.frontmatter.date} — ${m.frontmatter.title}\n ${m.path}`).join("\n\n")
670
+ : `No results for "${query}". (Note: advanced research features require the CLI.)`;
671
+ return { content: [{ type: "text", text }] };
672
+ }
540
673
  const args = ["research", query];
541
674
  if (contentType)
542
675
  args.push("-t", contentType);
@@ -629,6 +762,9 @@ server.tool("process_audio", "Process an audio file through the transcription pi
629
762
  type: z.enum(["meeting", "memo"]).optional().default("memo").describe("Content type"),
630
763
  title: z.string().optional().describe("Optional title"),
631
764
  }, { title: "Process Audio", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ file_path, type: contentType, title }) => {
765
+ if (!(await isCliAvailable())) {
766
+ return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
767
+ }
632
768
  const allowedDirs = [
633
769
  join(MINUTES_HOME, "inbox"),
634
770
  MEETINGS_DIR,
@@ -667,6 +803,9 @@ server.tool("add_note", "Add a note to the current recording. Notes are timestam
667
803
  .optional()
668
804
  .describe("Path to an existing meeting file to annotate (for post-meeting notes)"),
669
805
  }, { title: "Add Note", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async ({ text, meeting_path }) => {
806
+ if (!(await isCliAvailable())) {
807
+ return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
808
+ }
670
809
  try {
671
810
  const args = ["note", text];
672
811
  if (meeting_path) {
@@ -775,31 +914,121 @@ server.tool("register_qmd_collection", "Register the Minutes output directory as
775
914
  });
776
915
  // ── Resources ───────────────────────────────────────────────
777
916
  server.resource("recent_meetings", "minutes://meetings/recent", { description: "List of recent meetings and memos" }, async () => {
917
+ if (!(await isCliAvailable())) {
918
+ const meetings = await reader.listMeetings(MEETINGS_DIR, 20);
919
+ const json = JSON.stringify(meetings.map((m) => ({
920
+ date: m.frontmatter.date, title: m.frontmatter.title,
921
+ content_type: m.frontmatter.type, path: m.path, duration: m.frontmatter.duration,
922
+ })));
923
+ return { contents: [{ uri: "minutes://meetings/recent", mimeType: "application/json", text: json }] };
924
+ }
778
925
  const { stdout } = await runMinutes(["list", "--limit", "20"]);
779
926
  return { contents: [{ uri: "minutes://meetings/recent", mimeType: "application/json", text: stdout }] };
780
927
  });
781
928
  server.resource("recording_status", "minutes://status", { description: "Current recording status" }, async () => {
929
+ if (!(await isCliAvailable())) {
930
+ return { contents: [{ uri: "minutes://status", mimeType: "application/json", text: JSON.stringify({ recording: false, processing: false, note: "Read-only mode (CLI not installed)" }) }] };
931
+ }
782
932
  const { stdout } = await runMinutes(["status"]);
783
933
  return { contents: [{ uri: "minutes://status", mimeType: "application/json", text: stdout }] };
784
934
  });
785
935
  server.resource("open_actions", "minutes://actions/open", { description: "All open action items across meetings" }, async () => {
936
+ if (!(await isCliAvailable())) {
937
+ const actions = await reader.findOpenActions(MEETINGS_DIR);
938
+ return { contents: [{ uri: "minutes://actions/open", mimeType: "application/json", text: JSON.stringify(actions) }] };
939
+ }
786
940
  const { stdout } = await runMinutes(["search", "", "--intents-only", "--intent-kind", "action-item"]);
787
941
  return { contents: [{ uri: "minutes://actions/open", mimeType: "application/json", text: stdout }] };
788
942
  });
789
943
  server.resource("recent_events", "minutes://events/recent", { description: "Recent pipeline events (recordings, processing, notes)" }, async () => {
944
+ if (!(await isCliAvailable())) {
945
+ return { contents: [{ uri: "minutes://events/recent", mimeType: "application/json", text: "[]" }] };
946
+ }
790
947
  const { stdout } = await runMinutes(["events", "--limit", "20"]);
791
948
  return { contents: [{ uri: "minutes://events/recent", mimeType: "application/json", text: stdout }] };
792
949
  });
793
950
  server.resource("meeting", new ResourceTemplate("minutes://meetings/{slug}", { list: undefined }), { description: "Get a specific meeting by its filename slug" }, async (uri, variables) => {
794
951
  const slug = String(variables.slug);
952
+ if (!(await isCliAvailable())) {
953
+ // Without CLI resolve, find by filename match
954
+ const meetings = await reader.listMeetings(MEETINGS_DIR, 1000);
955
+ const match = meetings.find((m) => m.path.includes(slug));
956
+ if (match) {
957
+ const content = await readFile(match.path, "utf-8");
958
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: content }] };
959
+ }
960
+ return { contents: [{ uri: uri.href, mimeType: "text/plain", text: `Meeting not found: ${slug}` }] };
961
+ }
795
962
  const { stdout } = await runMinutes(["resolve", slug]);
796
963
  const parsed = parseJsonOutput(stdout);
797
964
  if (parsed.path) {
798
- const content = await readFile(parsed.path, "utf-8");
965
+ const validated = validatePathInDirectory(parsed.path, MEETINGS_DIR, [".md"]);
966
+ const content = await readFile(validated, "utf-8");
799
967
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: content }] };
800
968
  }
801
969
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: `Meeting not found: ${slug}` }] };
802
970
  });
971
+ // ── Tool: start_dictation ──────────────────────────────────
972
+ server.tool("start_dictation", "Start dictation mode. Speak naturally — text goes to clipboard and daily note after each pause. Runs until stop_dictation is called or silence timeout.", {}, { title: "Start Dictation", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async () => {
973
+ if (!(await isCliAvailable())) {
974
+ return { content: [{ type: "text", text: CLI_INSTALL_MSG }] };
975
+ }
976
+ const { stdout: statusOut } = await runMinutes(["status"]);
977
+ const status = parseJsonOutput(statusOut);
978
+ if (status.recording) {
979
+ return {
980
+ content: [
981
+ {
982
+ type: "text",
983
+ text: "Recording in progress — stop recording before dictating.",
984
+ },
985
+ ],
986
+ };
987
+ }
988
+ // Spawn detached dictation process
989
+ const child = spawn(MINUTES_BIN, ["dictate"], {
990
+ detached: true,
991
+ stdio: "ignore",
992
+ env: { ...process.env, RUST_LOG: "info" },
993
+ });
994
+ child.unref();
995
+ // Wait briefly for startup
996
+ await new Promise((r) => setTimeout(r, 500));
997
+ return {
998
+ content: [
999
+ {
1000
+ type: "text",
1001
+ text: "Dictation started. Speak naturally — text will be copied to clipboard after each pause. Say \"stop dictation\" when done.",
1002
+ },
1003
+ ],
1004
+ };
1005
+ });
1006
+ // ── Tool: stop_dictation ───────────────────────────────────
1007
+ server.tool("stop_dictation", "Stop the current dictation session.", {}, { title: "Stop Dictation", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, async () => {
1008
+ // Send stop signal by killing the dictation process via PID file
1009
+ const minutesDir = join(homedir(), ".minutes");
1010
+ const pidPath = join(minutesDir, "dictation.pid");
1011
+ if (existsSync(pidPath)) {
1012
+ try {
1013
+ const pidContent = await readFile(pidPath, "utf-8");
1014
+ const pid = parseInt(pidContent.trim(), 10);
1015
+ if (Number.isFinite(pid) && pid > 0) {
1016
+ process.kill(pid, "SIGTERM");
1017
+ }
1018
+ }
1019
+ catch {
1020
+ // Process already dead or PID file invalid
1021
+ }
1022
+ }
1023
+ return {
1024
+ content: [
1025
+ {
1026
+ type: "text",
1027
+ text: "Dictation stop requested.",
1028
+ },
1029
+ ],
1030
+ };
1031
+ });
803
1032
  // ── Start server ────────────────────────────────────────────
804
1033
  async function main() {
805
1034
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minutes-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "MCP server for minutes — conversation memory for AI assistants. Works with Claude Desktop, Cursor, Windsurf, and any MCP client.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
- "dist-ui/"
12
+ "dist-ui/",
13
+ "LICENSE"
13
14
  ],
14
15
  "keywords": [
15
16
  "mcp",
@@ -38,11 +39,11 @@
38
39
  },
39
40
  "dependencies": {
40
41
  "@modelcontextprotocol/sdk": "^1.27.1",
41
- "minutes-sdk": "file:../sdk",
42
+ "@modelcontextprotocol/ext-apps": "^1.2.2",
43
+ "minutes-sdk": "^0.5.0",
42
44
  "yaml": "^2.8.3"
43
45
  },
44
46
  "devDependencies": {
45
- "@modelcontextprotocol/ext-apps": "^1.2.2",
46
47
  "@types/node": "^22.0.0",
47
48
  "tsx": "^4.0.0",
48
49
  "typescript": "^5.5.0",