@open-recording/mcp 0.2.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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +229 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon Liu
|
|
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/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @open-recording/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [open-recording](https://github.com/simonliu-ai-product/open-recording) — lets any agent framework press record, stop, transcribe, and read the transcript back.
|
|
4
|
+
|
|
5
|
+
Mounted on the dev server by `open-recording dev --mcp`. It has to run in that process: the recorder state machine lives there, and the tools reach the browser studio through its open event stream.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @open-recording/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [repo README](https://github.com/simonliu-ai-product/open-recording#readme) for the tool list.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { OpenRecordingConfig } from "@open-recording/core";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
type OpenRecordingMcpOptions = {
|
|
5
|
+
/** The workspace root — the directory holding `recordings/`. */
|
|
6
|
+
userCwd: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
config?: OpenRecordingConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Extra hostnames allowed in Host/Origin headers. Loopback is always allowed;
|
|
11
|
+
* add to this only when the endpoint is deliberately exposed.
|
|
12
|
+
*/
|
|
13
|
+
allowedHosts?: string[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A fresh server per request. The recorder state it acts on is not held here —
|
|
17
|
+
* it lives in the dev server process this endpoint is mounted on — so there is
|
|
18
|
+
* nothing worth carrying between calls, which is the stateless shape that lets
|
|
19
|
+
* any client connect without a session handshake.
|
|
20
|
+
*/
|
|
21
|
+
declare function createOpenRecordingMcpServer(opts: OpenRecordingMcpOptions): McpServer;
|
|
22
|
+
declare function createOpenRecordingMcpHandler(opts: OpenRecordingMcpOptions): {
|
|
23
|
+
/**
|
|
24
|
+
* These tools hold a microphone and write to the user's disk, so a page in
|
|
25
|
+
* the browser must not be able to drive them. Rejecting unexpected Host and
|
|
26
|
+
* Origin headers is what closes the DNS-rebinding path onto a loopback
|
|
27
|
+
* endpoint.
|
|
28
|
+
*/
|
|
29
|
+
fetch: (request: Request) => Promise<Response>;
|
|
30
|
+
close: () => Promise<void>;
|
|
31
|
+
notify: import("@modelcontextprotocol/server").ServerNotifier;
|
|
32
|
+
bus: import("@modelcontextprotocol/server").ServerEventBus;
|
|
33
|
+
};
|
|
34
|
+
/** Connect/Express-style middleware, for mounting on the dev server. */
|
|
35
|
+
declare function createOpenRecordingMcpMiddleware(opts: OpenRecordingMcpOptions): import("@modelcontextprotocol/node").NodeMcpRequestHandler;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { OpenRecordingMcpOptions, createOpenRecordingMcpHandler, createOpenRecordingMcpMiddleware, createOpenRecordingMcpServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
2
|
+
import { McpServer, createMcpHandler, hostHeaderValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse } from "@modelcontextprotocol/server";
|
|
3
|
+
import { OpsError, cancelRecording, deleteRecording, listRecordings, makeContext, pauseRecording, readNotes, readRecording, readSubtitles, readTranscript, recorderStatus, renameRecording, resumeRecording, searchTranscripts, startRecording, stopRecording, tagRecording, transcribeEnvironment, transcribeRecording, writeNotes } from "@open-recording/core/ops";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/tools.ts
|
|
6
|
+
/**
|
|
7
|
+
* Every tool is a thin wrapper over `@open-recording/core/ops` — the same
|
|
8
|
+
* functions the dev server calls for the studio page. An agent and a person
|
|
9
|
+
* therefore share one recorder: whoever presses stop, the other one sees it.
|
|
10
|
+
*/
|
|
11
|
+
/** Tools return text; JSON payloads go through as pretty-printed text blocks. */
|
|
12
|
+
function ok(value) {
|
|
13
|
+
return { content: [{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
16
|
+
}] };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An OpsError is a refusal the caller can act on (503 no studio connected, 409
|
|
20
|
+
* already recording), so it comes back as a tool error rather than a transport
|
|
21
|
+
* failure.
|
|
22
|
+
*/
|
|
23
|
+
async function run(fn) {
|
|
24
|
+
try {
|
|
25
|
+
return ok(await fn());
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err instanceof OpsError) return {
|
|
28
|
+
isError: true,
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `${err.status}: ${err.message}`
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function registerTools(server, ctx) {
|
|
38
|
+
server.registerTool("recorder_status", {
|
|
39
|
+
title: "Recorder status",
|
|
40
|
+
description: "Whether anything is recording or paused, and whether a studio page is connected to record through. Check this before starting.",
|
|
41
|
+
inputSchema: z.object({})
|
|
42
|
+
}, () => run(() => recorderStatus(ctx)));
|
|
43
|
+
server.registerTool("start_recording", {
|
|
44
|
+
title: "Start recording",
|
|
45
|
+
description: "Press record on whatever the studio is holding — a microphone, or a browser tab with its picture and audio. Returns once the studio confirms it is capturing; fails if no studio is connected or nothing is armed. One recording at a time.",
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
title: z.string().optional().describe("what this recording is, e.g. \"Weekly sync\""),
|
|
48
|
+
tags: z.array(z.string()).optional(),
|
|
49
|
+
note: z.string().optional().describe("context worth keeping beside the audio")
|
|
50
|
+
})
|
|
51
|
+
}, ({ title, tags, note }) => run(() => startRecording(ctx, {
|
|
52
|
+
...title ? { title } : {},
|
|
53
|
+
...tags ? { tags } : {},
|
|
54
|
+
...note ? { note } : {}
|
|
55
|
+
})));
|
|
56
|
+
server.registerTool("stop_recording", {
|
|
57
|
+
title: "Stop recording",
|
|
58
|
+
description: "Stop the open recording and finalize the file — from paused too, with no need to resume first. Pass transcribe to run whisper.cpp immediately, which blocks until the transcription finishes.",
|
|
59
|
+
inputSchema: z.object({
|
|
60
|
+
transcribe: z.boolean().optional().describe("transcribe as soon as the audio lands"),
|
|
61
|
+
language: z.string().optional().describe("language code for the transcription, or `auto`")
|
|
62
|
+
})
|
|
63
|
+
}, ({ transcribe, language }) => run(async () => {
|
|
64
|
+
const meta = await stopRecording(ctx);
|
|
65
|
+
if (!transcribe) return meta;
|
|
66
|
+
const result = await transcribeRecording(ctx, meta.id, { ...language ? { language } : {} });
|
|
67
|
+
return {
|
|
68
|
+
...meta,
|
|
69
|
+
transcript: result
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
server.registerTool("pause_recording", {
|
|
73
|
+
title: "Pause recording",
|
|
74
|
+
description: "Pause without closing the recording — the audio stays one file, with the paused span simply absent from it. Use this for an interruption; use stop_recording when the session is over.",
|
|
75
|
+
inputSchema: z.object({})
|
|
76
|
+
}, () => run(() => pauseRecording(ctx)));
|
|
77
|
+
server.registerTool("resume_recording", {
|
|
78
|
+
title: "Resume recording",
|
|
79
|
+
description: "Carry on capturing into the recording that is paused.",
|
|
80
|
+
inputSchema: z.object({})
|
|
81
|
+
}, () => run(() => resumeRecording(ctx)));
|
|
82
|
+
server.registerTool("cancel_recording", {
|
|
83
|
+
title: "Cancel recording",
|
|
84
|
+
description: "Stop the open recording and delete its audio. Use when the recording was a mistake.",
|
|
85
|
+
inputSchema: z.object({})
|
|
86
|
+
}, () => run(() => cancelRecording(ctx)));
|
|
87
|
+
server.registerTool("list_recordings", {
|
|
88
|
+
title: "List recordings",
|
|
89
|
+
description: "Every recording with its id, title, length, and whether it has been transcribed.",
|
|
90
|
+
inputSchema: z.object({})
|
|
91
|
+
}, () => run(() => listRecordings(ctx)));
|
|
92
|
+
server.registerTool("read_recording", {
|
|
93
|
+
title: "Read recording metadata",
|
|
94
|
+
description: "Full metadata for one recording, including transcription details when present.",
|
|
95
|
+
inputSchema: z.object({ id: z.string() })
|
|
96
|
+
}, ({ id }) => run(() => readRecording(ctx, id)));
|
|
97
|
+
server.registerTool("transcribe_recording", {
|
|
98
|
+
title: "Transcribe recording",
|
|
99
|
+
description: "Run local whisper.cpp over a recording and store the transcript beside the audio. Refuses when a transcript exists unless force is set.",
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
id: z.string(),
|
|
102
|
+
language: z.string().optional().describe("language code, or `auto` (the default)"),
|
|
103
|
+
model: z.string().optional().describe("path to a ggml model, overriding the configured one"),
|
|
104
|
+
force: z.boolean().optional()
|
|
105
|
+
})
|
|
106
|
+
}, ({ id, language, model, force }) => run(() => transcribeRecording(ctx, id, {
|
|
107
|
+
...language ? { language } : {},
|
|
108
|
+
...model ? { model } : {},
|
|
109
|
+
...force ? { force } : {}
|
|
110
|
+
})));
|
|
111
|
+
server.registerTool("read_transcript", {
|
|
112
|
+
title: "Read transcript",
|
|
113
|
+
description: "The stored transcript. `markdown` (default) carries timestamps you can cite; `text` is prose only; `segments` is the timed data.",
|
|
114
|
+
inputSchema: z.object({
|
|
115
|
+
id: z.string(),
|
|
116
|
+
view: z.enum([
|
|
117
|
+
"markdown",
|
|
118
|
+
"text",
|
|
119
|
+
"segments"
|
|
120
|
+
]).optional()
|
|
121
|
+
})
|
|
122
|
+
}, ({ id, view }) => run(() => readTranscript(ctx, id, view ?? "markdown")));
|
|
123
|
+
server.registerTool("read_subtitles", {
|
|
124
|
+
title: "Read subtitles",
|
|
125
|
+
description: "The transcript as subtitle cues — `srt` for an editor, `vtt` for a player. Written when a recording is transcribed.",
|
|
126
|
+
inputSchema: z.object({
|
|
127
|
+
id: z.string(),
|
|
128
|
+
format: z.enum(["srt", "vtt"]).optional()
|
|
129
|
+
})
|
|
130
|
+
}, ({ id, format }) => run(() => readSubtitles(ctx, id, format ?? "srt")));
|
|
131
|
+
server.registerTool("search_transcripts", {
|
|
132
|
+
title: "Search transcripts",
|
|
133
|
+
description: "Substring search across every stored transcript. Hits carry the recording id and the millisecond offset, so you can point at the moment.",
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
query: z.string(),
|
|
136
|
+
limit: z.number().int().positive().max(100).optional()
|
|
137
|
+
})
|
|
138
|
+
}, ({ query, limit }) => run(() => searchTranscripts(ctx, query, limit ?? 20)));
|
|
139
|
+
server.registerTool("write_notes", {
|
|
140
|
+
title: "Write notes",
|
|
141
|
+
description: "Store your own Markdown beside the audio — a summary, action items, decisions. Overwrites any previous notes.",
|
|
142
|
+
inputSchema: z.object({
|
|
143
|
+
id: z.string(),
|
|
144
|
+
notes: z.string()
|
|
145
|
+
})
|
|
146
|
+
}, ({ id, notes }) => run(() => writeNotes(ctx, id, notes)));
|
|
147
|
+
server.registerTool("read_notes", {
|
|
148
|
+
title: "Read notes",
|
|
149
|
+
description: "The Markdown notes stored beside a recording.",
|
|
150
|
+
inputSchema: z.object({ id: z.string() })
|
|
151
|
+
}, ({ id }) => run(() => readNotes(ctx, id)));
|
|
152
|
+
server.registerTool("rename_recording", {
|
|
153
|
+
title: "Rename recording",
|
|
154
|
+
description: "Change a recording’s title. The id never moves — it is the folder on disk.",
|
|
155
|
+
inputSchema: z.object({
|
|
156
|
+
id: z.string(),
|
|
157
|
+
title: z.string()
|
|
158
|
+
})
|
|
159
|
+
}, ({ id, title }) => run(() => renameRecording(ctx, id, title)));
|
|
160
|
+
server.registerTool("tag_recording", {
|
|
161
|
+
title: "Tag recording",
|
|
162
|
+
description: "Replace a recording’s tags.",
|
|
163
|
+
inputSchema: z.object({
|
|
164
|
+
id: z.string(),
|
|
165
|
+
tags: z.array(z.string())
|
|
166
|
+
})
|
|
167
|
+
}, ({ id, tags }) => run(() => tagRecording(ctx, id, tags)));
|
|
168
|
+
server.registerTool("delete_recording", {
|
|
169
|
+
title: "Delete recording",
|
|
170
|
+
description: "Permanently delete a recording — audio, transcript, and notes. Confirm with the user first.",
|
|
171
|
+
inputSchema: z.object({ id: z.string() })
|
|
172
|
+
}, ({ id }) => run(() => deleteRecording(ctx, id)));
|
|
173
|
+
server.registerTool("transcription_environment", {
|
|
174
|
+
title: "Transcription environment",
|
|
175
|
+
description: "Which whisper.cpp binary, model, and ffmpeg the workspace resolved. Read this when a transcription is refused with a 503.",
|
|
176
|
+
inputSchema: z.object({})
|
|
177
|
+
}, () => run(() => transcribeEnvironment(ctx)));
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/index.ts
|
|
181
|
+
function contextFor(opts) {
|
|
182
|
+
return makeContext({
|
|
183
|
+
userCwd: opts.userCwd,
|
|
184
|
+
coreVersion: opts.version ?? "0.0.0",
|
|
185
|
+
...opts.config ? { config: opts.config } : {}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A fresh server per request. The recorder state it acts on is not held here —
|
|
190
|
+
* it lives in the dev server process this endpoint is mounted on — so there is
|
|
191
|
+
* nothing worth carrying between calls, which is the stateless shape that lets
|
|
192
|
+
* any client connect without a session handshake.
|
|
193
|
+
*/
|
|
194
|
+
function createOpenRecordingMcpServer(opts) {
|
|
195
|
+
const server = new McpServer({
|
|
196
|
+
name: "open-recording",
|
|
197
|
+
version: opts.version ?? "0.0.0",
|
|
198
|
+
title: "open-recording"
|
|
199
|
+
});
|
|
200
|
+
registerTools(server, contextFor(opts));
|
|
201
|
+
return server;
|
|
202
|
+
}
|
|
203
|
+
function createOpenRecordingMcpHandler(opts) {
|
|
204
|
+
const allowedHostnames = [...localhostAllowedHostnames(), ...opts.allowedHosts ?? []];
|
|
205
|
+
const allowedOrigins = [...localhostAllowedOrigins(), ...opts.allowedHosts ?? []];
|
|
206
|
+
const handler = createMcpHandler(() => createOpenRecordingMcpServer(opts));
|
|
207
|
+
return {
|
|
208
|
+
...handler,
|
|
209
|
+
/**
|
|
210
|
+
* These tools hold a microphone and write to the user's disk, so a page in
|
|
211
|
+
* the browser must not be able to drive them. Rejecting unexpected Host and
|
|
212
|
+
* Origin headers is what closes the DNS-rebinding path onto a loopback
|
|
213
|
+
* endpoint.
|
|
214
|
+
*/
|
|
215
|
+
fetch: async (request) => {
|
|
216
|
+
const hostRejection = hostHeaderValidationResponse(request, allowedHostnames);
|
|
217
|
+
if (hostRejection) return hostRejection;
|
|
218
|
+
const originRejection = originValidationResponse(request, allowedOrigins);
|
|
219
|
+
if (originRejection) return originRejection;
|
|
220
|
+
return handler.fetch(request);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Connect/Express-style middleware, for mounting on the dev server. */
|
|
225
|
+
function createOpenRecordingMcpMiddleware(opts) {
|
|
226
|
+
return toNodeHandler(createOpenRecordingMcpHandler(opts));
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { createOpenRecordingMcpHandler, createOpenRecordingMcpMiddleware, createOpenRecordingMcpServer };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-recording/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for open-recording — lets any agent framework press record, stop, transcribe, and read back the transcript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"recording",
|
|
23
|
+
"transcription",
|
|
24
|
+
"agent"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Simon Liu",
|
|
29
|
+
"url": "https://github.com/LiuYuWei"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/simonliu-ai-product/open-recording",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/simonliu-ai-product/open-recording.git",
|
|
35
|
+
"directory": "packages/mcp"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/simonliu-ai-product/open-recording/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
45
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
46
|
+
"zod": "^4.4.3"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@open-recording/core": "^0.2.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.19.17",
|
|
53
|
+
"tsdown": "^0.22.14",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"@open-recording/core": "0.2.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|