@menutes/mcp-server 0.1.0

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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @menutes/mcp-server
2
+
3
+ MCP server for accessing [Menutes](https://menutes.com) meeting transcripts and summaries from Claude Code.
4
+
5
+ ## Setup
6
+
7
+ 1. Get an API key from [Settings](https://app.menutes.com/settings?tab=api-keys)
8
+
9
+ 2. Add to your MCP config:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "menutes": {
15
+ "command": "npx",
16
+ "args": ["@menutes/mcp-server"],
17
+ "env": {
18
+ "MENUTES_API_KEY": "mnts_your_key_here"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## Tools
26
+
27
+ | Tool | Description |
28
+ |------|-------------|
29
+ | `list_recordings` | List meetings with optional filtering by status and scope |
30
+ | `get_recording` | Get metadata for a specific recording |
31
+ | `get_transcript` | Get speaker-labeled transcript with timestamps |
32
+ | `get_summary` | Get AI-generated summary with action items |
33
+ | `search_recordings` | Search recordings by title |
34
+
35
+ ## Environment Variables
36
+
37
+ | Variable | Required | Default | Description |
38
+ |----------|----------|---------|-------------|
39
+ | `MENUTES_API_KEY` | Yes | - | API key (starts with `mnts_`) |
40
+ | `MENUTES_BASE_URL` | No | `https://app.menutes.com` | API base URL |
41
+
42
+ ## License
43
+
44
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ export interface Recording {
2
+ id: string;
3
+ meetingTitle: string | null;
4
+ status: "UPLOADING" | "PROCESSING" | "COMPLETED" | "FAILED";
5
+ duration: number | null;
6
+ speakerCount: number | null;
7
+ sharingScope: string;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ processingPhase: string | null;
11
+ sourceType: string;
12
+ user: {
13
+ id: string;
14
+ name: string | null;
15
+ };
16
+ team: {
17
+ id: string;
18
+ name: string;
19
+ } | null;
20
+ isOwner: boolean;
21
+ }
22
+ export interface RecordingContent {
23
+ transcription: string | null;
24
+ summary: string | null;
25
+ transcriptSegments: {
26
+ speaker: string;
27
+ text: string;
28
+ startTime: number | null;
29
+ endTime: number | null;
30
+ }[];
31
+ speakerNames: Record<string, string>;
32
+ notes: string | null;
33
+ }
34
+ export interface ListRecordingsResponse {
35
+ recordings: Recording[];
36
+ pagination: {
37
+ page: number;
38
+ limit: number;
39
+ total: number;
40
+ totalPages: number;
41
+ hasNextPage: boolean;
42
+ hasPrevPage: boolean;
43
+ };
44
+ }
45
+ export interface SearchResponse {
46
+ recordings: Recording[];
47
+ total: number;
48
+ }
49
+ export declare class MenutesApiClient {
50
+ private baseUrl;
51
+ private apiKey;
52
+ constructor(baseUrl: string, apiKey: string);
53
+ private request;
54
+ listRecordings(params?: {
55
+ page?: number;
56
+ limit?: number;
57
+ status?: string;
58
+ view?: string;
59
+ }): Promise<ListRecordingsResponse>;
60
+ getRecording(id: string): Promise<Recording>;
61
+ getRecordingContent(id: string): Promise<RecordingContent>;
62
+ searchRecordings(query: string, limit?: number): Promise<SearchResponse>;
63
+ }
package/dist/api.js ADDED
@@ -0,0 +1,66 @@
1
+ export class MenutesApiClient {
2
+ baseUrl;
3
+ apiKey;
4
+ constructor(baseUrl, apiKey) {
5
+ this.baseUrl = baseUrl;
6
+ this.apiKey = apiKey;
7
+ }
8
+ async request(path, params) {
9
+ const url = new URL(path, this.baseUrl);
10
+ if (params) {
11
+ for (const [key, value] of Object.entries(params)) {
12
+ if (value !== undefined && value !== null) {
13
+ url.searchParams.set(key, String(value));
14
+ }
15
+ }
16
+ }
17
+ let response;
18
+ try {
19
+ response = await fetch(url.toString(), {
20
+ headers: { Authorization: `Bearer ${this.apiKey}` },
21
+ });
22
+ }
23
+ catch {
24
+ throw new Error(`Could not connect to Menutes API at ${this.baseUrl}`);
25
+ }
26
+ if (!response.ok) {
27
+ if (response.status === 401) {
28
+ throw new Error("Invalid API key. Check your MENUTES_API_KEY.");
29
+ }
30
+ if (response.status === 403) {
31
+ throw new Error("You don't have access to this recording.");
32
+ }
33
+ if (response.status === 404) {
34
+ throw new Error("Recording not found.");
35
+ }
36
+ if (response.status === 429) {
37
+ throw new Error("Rate limit exceeded (1000 req/hour). Try again later.");
38
+ }
39
+ const body = await response.text();
40
+ let message;
41
+ try {
42
+ message = JSON.parse(body).error || body;
43
+ }
44
+ catch {
45
+ message = body || `HTTP ${response.status}`;
46
+ }
47
+ throw new Error(`Menutes API error: ${message}`);
48
+ }
49
+ return (await response.json());
50
+ }
51
+ async listRecordings(params) {
52
+ return this.request("/api/v1/recordings", params);
53
+ }
54
+ async getRecording(id) {
55
+ return this.request(`/api/v1/recordings/${encodeURIComponent(id)}`);
56
+ }
57
+ async getRecordingContent(id) {
58
+ return this.request(`/api/v1/recordings/${encodeURIComponent(id)}/content`);
59
+ }
60
+ async searchRecordings(query, limit) {
61
+ const params = { q: query };
62
+ if (limit !== undefined)
63
+ params.limit = limit;
64
+ return this.request("/api/v1/recordings/search", params);
65
+ }
66
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { MenutesApiClient } from "./api.js";
4
+ import { createServer } from "./server.js";
5
+ const apiKey = process.env.MENUTES_API_KEY;
6
+ if (!apiKey) {
7
+ console.error("MENUTES_API_KEY environment variable is required");
8
+ process.exit(1);
9
+ }
10
+ if (!apiKey.startsWith("mnts_")) {
11
+ console.error("MENUTES_API_KEY must start with 'mnts_'");
12
+ process.exit(1);
13
+ }
14
+ const baseUrl = process.env.MENUTES_BASE_URL || "https://app.menutes.com";
15
+ const api = new MenutesApiClient(baseUrl, apiKey);
16
+ const server = createServer(api);
17
+ const transport = new StdioServerTransport();
18
+ await server.connect(transport);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "./api.js";
3
+ export declare function createServer(api: MenutesApiClient): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerListRecordings } from "./tools/list-recordings.js";
3
+ import { registerGetRecording } from "./tools/get-recording.js";
4
+ import { registerGetTranscript } from "./tools/get-transcript.js";
5
+ import { registerGetSummary } from "./tools/get-summary.js";
6
+ import { registerSearchRecordings } from "./tools/search-recordings.js";
7
+ export function createServer(api) {
8
+ const server = new McpServer({
9
+ name: "menutes",
10
+ version: "0.1.0",
11
+ });
12
+ registerListRecordings(server, api);
13
+ registerGetRecording(server, api);
14
+ registerGetTranscript(server, api);
15
+ registerGetSummary(server, api);
16
+ registerSearchRecordings(server, api);
17
+ return server;
18
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "../api.js";
3
+ export declare function registerGetRecording(server: McpServer, api: MenutesApiClient): void;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ export function registerGetRecording(server, api) {
3
+ server.tool("get_recording", "Get detailed metadata for a specific Menutes recording including title, date, duration, speaker count, status, and sharing scope.", {
4
+ id: z.string().describe("The recording ID"),
5
+ }, async ({ id }) => {
6
+ try {
7
+ const r = await api.getRecording(id);
8
+ const duration = r.duration
9
+ ? `${Math.floor(r.duration / 60)}m ${r.duration % 60}s`
10
+ : "unknown";
11
+ const date = new Date(r.createdAt).toLocaleString("en-GB", {
12
+ day: "numeric",
13
+ month: "short",
14
+ year: "numeric",
15
+ hour: "2-digit",
16
+ minute: "2-digit",
17
+ });
18
+ const text = [
19
+ `**${r.meetingTitle || "Untitled"}**`,
20
+ `Date: ${date}`,
21
+ `Duration: ${duration}`,
22
+ `Speakers: ${r.speakerCount ?? "unknown"}`,
23
+ `Status: ${r.status}`,
24
+ `Sharing: ${r.sharingScope}`,
25
+ `Source: ${r.sourceType}`,
26
+ `Owner: ${r.user.name || r.user.id}${r.team ? ` (${r.team.name})` : ""}`,
27
+ `ID: ${r.id}`,
28
+ ].join("\n");
29
+ return { content: [{ type: "text", text }] };
30
+ }
31
+ catch (error) {
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "../api.js";
3
+ export declare function registerGetSummary(server: McpServer, api: MenutesApiClient): void;
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ export function registerGetSummary(server, api) {
3
+ server.tool("get_summary", "Get the AI-generated summary of a Menutes recording including discussion points, decisions, and action items.", {
4
+ id: z.string().describe("The recording ID"),
5
+ }, async ({ id }) => {
6
+ try {
7
+ const content = await api.getRecordingContent(id);
8
+ if (!content.summary) {
9
+ return {
10
+ content: [
11
+ {
12
+ type: "text",
13
+ text: "No summary available for this recording.",
14
+ },
15
+ ],
16
+ };
17
+ }
18
+ return { content: [{ type: "text", text: content.summary }] };
19
+ }
20
+ catch (error) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "../api.js";
3
+ export declare function registerGetTranscript(server: McpServer, api: MenutesApiClient): void;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ function formatTime(seconds) {
3
+ if (seconds === null || seconds === undefined)
4
+ return "??:??";
5
+ const m = Math.floor(seconds / 60);
6
+ const s = Math.floor(seconds % 60);
7
+ return `${m}:${s.toString().padStart(2, "0")}`;
8
+ }
9
+ export function registerGetTranscript(server, api) {
10
+ server.tool("get_transcript", "Get the full speaker-labeled transcript with timestamps for a Menutes recording. For long meetings, consider using get_summary first.", {
11
+ id: z.string().describe("The recording ID"),
12
+ }, async ({ id }) => {
13
+ try {
14
+ const content = await api.getRecordingContent(id);
15
+ if (!content.transcriptSegments ||
16
+ content.transcriptSegments.length === 0) {
17
+ if (content.transcription) {
18
+ return {
19
+ content: [{ type: "text", text: content.transcription }],
20
+ };
21
+ }
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: "No transcript available for this recording.",
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ const names = content.speakerNames || {};
32
+ const lines = content.transcriptSegments.map((seg) => {
33
+ const name = names[seg.speaker] || seg.speaker;
34
+ const time = formatTime(seg.startTime);
35
+ return `[${name}] (${time}): ${seg.text}`;
36
+ });
37
+ return { content: [{ type: "text", text: lines.join("\n") }] };
38
+ }
39
+ catch (error) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
45
+ },
46
+ ],
47
+ isError: true,
48
+ };
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "../api.js";
3
+ export declare function registerListRecordings(server: McpServer, api: MenutesApiClient): void;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ export function registerListRecordings(server, api) {
3
+ server.tool("list_recordings", "List your Menutes meeting recordings with optional filtering. Returns titles, dates, durations, and IDs for further querying.", {
4
+ page: z
5
+ .number()
6
+ .int()
7
+ .positive()
8
+ .optional()
9
+ .describe("Page number (default: 1)"),
10
+ limit: z
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(100)
15
+ .optional()
16
+ .describe("Results per page (default: 20, max: 100)"),
17
+ status: z
18
+ .enum(["UPLOADING", "PROCESSING", "COMPLETED", "FAILED"])
19
+ .optional()
20
+ .describe("Filter by recording status"),
21
+ view: z
22
+ .enum(["my", "team", "organization", "all"])
23
+ .optional()
24
+ .describe("Scope: my (own), team (team-shared), organization (org-wide), all (admin)"),
25
+ }, async (params) => {
26
+ try {
27
+ const result = await api.listRecordings(params);
28
+ const lines = result.recordings.map((r) => {
29
+ const duration = r.duration
30
+ ? `${Math.floor(r.duration / 60)}m ${r.duration % 60}s`
31
+ : "unknown";
32
+ const date = new Date(r.createdAt).toLocaleDateString("en-GB", {
33
+ day: "numeric",
34
+ month: "short",
35
+ year: "numeric",
36
+ });
37
+ return `- **${r.meetingTitle || "Untitled"}** (${date}, ${duration}, ${r.speakerCount ?? "?"} speakers) [${r.status}]\n ID: ${r.id}`;
38
+ });
39
+ const { pagination: p } = result;
40
+ const header = `Found ${p.total} recording(s) — page ${p.page}/${p.totalPages}`;
41
+ return {
42
+ content: [
43
+ { type: "text", text: `${header}\n\n${lines.join("\n")}` },
44
+ ],
45
+ };
46
+ }
47
+ catch (error) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MenutesApiClient } from "../api.js";
3
+ export declare function registerSearchRecordings(server: McpServer, api: MenutesApiClient): void;
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ export function registerSearchRecordings(server, api) {
3
+ server.tool("search_recordings", "Search Menutes recordings by title. Returns matching recordings with IDs for further querying.", {
4
+ query: z.string().describe("Search query to match against titles"),
5
+ limit: z
6
+ .number()
7
+ .int()
8
+ .min(1)
9
+ .max(50)
10
+ .optional()
11
+ .describe("Max results (default: 10, max: 50)"),
12
+ }, async ({ query, limit }) => {
13
+ try {
14
+ const result = await api.searchRecordings(query, limit);
15
+ if (result.recordings.length === 0) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: `No recordings found matching "${query}".`,
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ const lines = result.recordings.map((r) => {
26
+ const date = new Date(r.createdAt).toLocaleDateString("en-GB", {
27
+ day: "numeric",
28
+ month: "short",
29
+ year: "numeric",
30
+ });
31
+ const duration = r.duration
32
+ ? `${Math.floor(r.duration / 60)}m ${r.duration % 60}s`
33
+ : "unknown";
34
+ return `- **${r.meetingTitle || "Untitled"}** (${date}, ${duration})\n ID: ${r.id}`;
35
+ });
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `Found ${result.total} result(s) for "${query}":\n\n${lines.join("\n")}`,
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
51
+ },
52
+ ],
53
+ isError: true,
54
+ };
55
+ }
56
+ });
57
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@menutes/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for accessing Menutes meeting transcripts and summaries from Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "menutes-mcp-server": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc && chmod +x dist/index.js",
14
+ "prepare": "npm run build",
15
+ "dev": "tsc --watch"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "menutes",
20
+ "meetings",
21
+ "transcription",
22
+ "claude-code",
23
+ "model-context-protocol"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.0",
31
+ "zod": "^3.24.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "typescript": "^5.7.0"
36
+ }
37
+ }