creavit-studio-mcp 1.0.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 +195 -0
- package/bin/creavit-mcp.mjs +64 -0
- package/package.json +39 -0
- package/src/bridgeClient.mjs +133 -0
- package/src/crvtReader.mjs +108 -0
- package/src/manifestSummary.mjs +88 -0
- package/src/protocol.mjs +24 -0
- package/src/rpcServer.mjs +118 -0
- package/src/tools/defineTool.mjs +59 -0
- package/src/tools/editorTools.mjs +192 -0
- package/src/tools/escapeTools.mjs +112 -0
- package/src/tools/index.mjs +53 -0
- package/src/tools/projectTools.mjs +90 -0
- package/src/tools/systemTools.mjs +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Creavit Studio MCP Server
|
|
2
|
+
|
|
3
|
+
Lets AI coding agents — Claude Code, Codex, Cursor, or anything else that speaks
|
|
4
|
+
[MCP](https://modelcontextprotocol.io) — drive the Creavit Studio desktop app:
|
|
5
|
+
open projects, change settings, add zoom ranges, start recordings, export video,
|
|
6
|
+
and **see the canvas** to verify the result.
|
|
7
|
+
|
|
8
|
+
macOS only (Creavit Studio is a macOS app). Requires Node 18+.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
### Claude Code
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
claude mcp add creavit-studio -- npx -y creavit-studio-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or, if you have the app repo checked out, `.mcp.json` in `desktop/` already
|
|
19
|
+
configures it — just open Claude Code there.
|
|
20
|
+
|
|
21
|
+
### Cursor
|
|
22
|
+
|
|
23
|
+
`~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"creavit-studio": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "creavit-studio-mcp"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Codex CLI
|
|
37
|
+
|
|
38
|
+
`~/.codex/config.toml`:
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[mcp_servers.creavit-studio]
|
|
42
|
+
command = "npx"
|
|
43
|
+
args = ["-y", "creavit-studio-mcp"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Verify
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx creavit-studio-mcp --doctor
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
creavit-studio v1.0.0
|
|
54
|
+
|
|
55
|
+
✓ 34 MCP tools defined
|
|
56
|
+
✓ Offline project reading available (.crvt files readable without the app)
|
|
57
|
+
✓ Connected to the app (v3.0.5, pid 61068)
|
|
58
|
+
Open windows: 2
|
|
59
|
+
Agent-connected renderers: main, editor
|
|
60
|
+
✓ 41 bridge commands available
|
|
61
|
+
|
|
62
|
+
Everything looks good.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Agent (Claude Code / Codex / Cursor)
|
|
69
|
+
│ MCP over stdio (JSON-RPC)
|
|
70
|
+
▼
|
|
71
|
+
creavit-studio-mcp
|
|
72
|
+
│ HTTP + bearer token, bound to 127.0.0.1 only
|
|
73
|
+
▼
|
|
74
|
+
Creavit Studio main process (agent bridge)
|
|
75
|
+
│ IPC
|
|
76
|
+
▼
|
|
77
|
+
Renderer windows (editor / recorder) → the real app state
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
On startup the app picks a random port, generates a fresh token, and writes an
|
|
81
|
+
endpoint descriptor:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
~/Library/Application Support/creavit-studio/agent-bridge.json (mode 0600)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The MCP server discovers the app through that file. Restarting the app does
|
|
88
|
+
**not** require restarting the MCP server — the endpoint is re-read on every
|
|
89
|
+
request.
|
|
90
|
+
|
|
91
|
+
The app must be running for most tools. Project inspection tools read `.crvt`
|
|
92
|
+
files directly and work with the app closed.
|
|
93
|
+
|
|
94
|
+
## Tools
|
|
95
|
+
|
|
96
|
+
**App** — `creavit_app_info`, `creavit_app_window`, `creavit_app_mode`,
|
|
97
|
+
`creavit_permissions`, `creavit_events`
|
|
98
|
+
|
|
99
|
+
**Projects** — `creavit_project_list`, `creavit_project_summary`,
|
|
100
|
+
`creavit_project_manifest`, `creavit_project_open`, `creavit_project_save`,
|
|
101
|
+
`creavit_project_reveal`
|
|
102
|
+
|
|
103
|
+
> The first three work with the app closed.
|
|
104
|
+
|
|
105
|
+
**Editor** — `creavit_editor_state`, `creavit_editor_get_settings`,
|
|
106
|
+
`creavit_editor_set_settings`, `creavit_editor_list_zooms`,
|
|
107
|
+
`creavit_editor_add_zoom`, `creavit_editor_update_zoom`,
|
|
108
|
+
`creavit_editor_remove_zoom`, `creavit_editor_segments`,
|
|
109
|
+
`creavit_editor_set_segments`, `creavit_editor_seek`,
|
|
110
|
+
`creavit_editor_playback`, `creavit_editor_screenshot`,
|
|
111
|
+
`creavit_editor_export`, `creavit_editor_history`
|
|
112
|
+
|
|
113
|
+
**Recording** — `creavit_devices_list`, `creavit_recording_status`,
|
|
114
|
+
`creavit_recording_start`, `creavit_recording_stop`
|
|
115
|
+
|
|
116
|
+
**Escape hatches** — `creavit_capabilities`, `creavit_call`, `creavit_ipc`,
|
|
117
|
+
`creavit_eval`, `creavit_logs`
|
|
118
|
+
|
|
119
|
+
The typed tools cannot cover the whole app. `creavit_capabilities` lists
|
|
120
|
+
**every** command and editor action the app exposes; `creavit_call` invokes any
|
|
121
|
+
of them; `creavit_ipc` reaches all ~110 raw IPC channels. New app features
|
|
122
|
+
become reachable through these without changing the MCP server.
|
|
123
|
+
|
|
124
|
+
## Typical flow
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
creavit_app_info is the app running, which windows are open
|
|
128
|
+
creavit_project_list see available projects
|
|
129
|
+
creavit_project_summary understand the target project
|
|
130
|
+
creavit_project_open open it in the editor
|
|
131
|
+
creavit_editor_state poll until duration > 0 (loading takes seconds)
|
|
132
|
+
creavit_editor_screenshot see the current look
|
|
133
|
+
creavit_editor_set_settings change something
|
|
134
|
+
creavit_editor_screenshot VERIFY the result
|
|
135
|
+
creavit_project_save persist
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Two things worth knowing:
|
|
139
|
+
|
|
140
|
+
- **Wait for the load.** After `creavit_project_open`, poll `creavit_editor_state`
|
|
141
|
+
until `duration > 0`. Settings written before loading finishes get overwritten.
|
|
142
|
+
- **`padding`, `radius`, `shadowSize` are derived values.** Reads and writes are
|
|
143
|
+
transparently redirected to `basePadding` / `baseRadius` / `baseShadowSize`,
|
|
144
|
+
and `creavit_editor_set_settings` returns a `verified` block showing what was
|
|
145
|
+
actually stored. Check it rather than assuming a write landed.
|
|
146
|
+
|
|
147
|
+
## Security
|
|
148
|
+
|
|
149
|
+
- Binds to `127.0.0.1` only; requests carrying an `Origin` header are rejected,
|
|
150
|
+
so no web page can reach it.
|
|
151
|
+
- Every request requires a bearer token, regenerated on each app launch.
|
|
152
|
+
- The endpoint file is written with `0600` permissions.
|
|
153
|
+
- Arbitrary code execution (`creavit_eval`) is **off by default**.
|
|
154
|
+
|
|
155
|
+
### Environment variables
|
|
156
|
+
|
|
157
|
+
Set on the **app**, not the MCP server:
|
|
158
|
+
|
|
159
|
+
| Variable | Effect |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `CREAVIT_AGENT_BRIDGE=0` | Disable the bridge entirely |
|
|
162
|
+
| `CREAVIT_AGENT_BRIDGE_PORT` | Pin a fixed port (default: pick a free one) |
|
|
163
|
+
| `CREAVIT_AGENT_BRIDGE_EVAL=1` | Enable the `creavit_eval` tool |
|
|
164
|
+
|
|
165
|
+
Set on the **MCP server**:
|
|
166
|
+
|
|
167
|
+
| Variable | Effect |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `CREAVIT_AGENT_BRIDGE_ENDPOINT` | Explicit path to the endpoint descriptor |
|
|
170
|
+
|
|
171
|
+
## Troubleshooting
|
|
172
|
+
|
|
173
|
+
**"Creavit Studio is not running"** — start the app. If it is running, check
|
|
174
|
+
that the bridge came up: its log line is `[AgentBridge] Ready → http://127.0.0.1:<port>`.
|
|
175
|
+
|
|
176
|
+
**"The 'editor' window is not open"** — editor tools need a project open. Call
|
|
177
|
+
`creavit_project_open` first.
|
|
178
|
+
|
|
179
|
+
**Agent says a tool does not exist** — MCP clients cache the tool list at
|
|
180
|
+
connect time. Restart the agent session after upgrading the server.
|
|
181
|
+
|
|
182
|
+
**Settings do not seem to apply** — check the `verified` field in the
|
|
183
|
+
`creavit_editor_set_settings` response, and make sure the project finished
|
|
184
|
+
loading (`creavit_editor_state` → `duration > 0`).
|
|
185
|
+
|
|
186
|
+
## Extending
|
|
187
|
+
|
|
188
|
+
The MCP server is a thin, typed façade. To add a capability:
|
|
189
|
+
|
|
190
|
+
1. **Main-process work** → add a command in `electron/agentBridge/commands/*.cjs`.
|
|
191
|
+
2. **Needs renderer state** → add an action to the relevant host composable in
|
|
192
|
+
`composables/agent/`, then publish it via `proxy("<action>")` in
|
|
193
|
+
`electron/agentBridge/commands/editorCommands.cjs`.
|
|
194
|
+
3. **Want a typed MCP tool** → add it in `mcp/src/tools/*.mjs`. Optional —
|
|
195
|
+
anything registered on the bridge is already reachable via `creavit_call`.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Creavit Studio MCP sunucusu.
|
|
3
|
+
// Claude Code / Codex / Cursor gibi MCP istemcilerinin uygulamayı sürebilmesi
|
|
4
|
+
// için stdio üzerinden çalışır.
|
|
5
|
+
//
|
|
6
|
+
// Teşhis: `npx creavit-studio-mcp --doctor`
|
|
7
|
+
|
|
8
|
+
import { startRpcServer, log } from "../src/rpcServer.mjs";
|
|
9
|
+
import { listTools, callTool } from "../src/tools/index.mjs";
|
|
10
|
+
import { health, listBridgeCommands } from "../src/bridgeClient.mjs";
|
|
11
|
+
import { canReadOffline } from "../src/crvtReader.mjs";
|
|
12
|
+
import { SERVER_NAME, SERVER_VERSION } from "../src/protocol.mjs";
|
|
13
|
+
|
|
14
|
+
async function doctor() {
|
|
15
|
+
console.log(`${SERVER_NAME} v${SERVER_VERSION}\n`);
|
|
16
|
+
|
|
17
|
+
const tools = await listTools();
|
|
18
|
+
console.log(`✓ ${tools.length} MCP tools defined`);
|
|
19
|
+
console.log(
|
|
20
|
+
canReadOffline()
|
|
21
|
+
? "✓ Offline project reading available (.crvt files readable without the app)"
|
|
22
|
+
: "! Offline project reading unavailable — install the `yauzl` dependency",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const info = await health();
|
|
27
|
+
console.log(`✓ Connected to the app (v${info.appVersion}, pid ${info.pid})`);
|
|
28
|
+
console.log(` Open windows: ${info.windows}`);
|
|
29
|
+
console.log(
|
|
30
|
+
` Agent-connected renderers: ${
|
|
31
|
+
info.rendererHosts?.map((h) => h.role).join(", ") || "none"
|
|
32
|
+
}`,
|
|
33
|
+
);
|
|
34
|
+
const { commands } = await listBridgeCommands();
|
|
35
|
+
console.log(`✓ ${commands.length} bridge commands available`);
|
|
36
|
+
console.log("\nEverything looks good.");
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log(`✗ Could not connect to the app: ${error.message}`);
|
|
39
|
+
console.log("\n Start Creavit Studio, then run this again.");
|
|
40
|
+
console.log(" Project inspection tools work without the app.");
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.argv.includes("--doctor")) {
|
|
46
|
+
await doctor();
|
|
47
|
+
} else if (process.argv.includes("--list-tools")) {
|
|
48
|
+
console.log(JSON.stringify(await listTools(), null, 2));
|
|
49
|
+
} else if (process.argv.includes("--version")) {
|
|
50
|
+
console.log(SERVER_VERSION);
|
|
51
|
+
} else if (process.argv.includes("--help")) {
|
|
52
|
+
console.log(
|
|
53
|
+
`${SERVER_NAME} v${SERVER_VERSION}\n\n` +
|
|
54
|
+
"MCP server that lets AI agents drive the Creavit Studio desktop app.\n\n" +
|
|
55
|
+
"Usage:\n" +
|
|
56
|
+
" creavit-studio-mcp Run as an MCP server over stdio (what MCP clients do)\n" +
|
|
57
|
+
" creavit-studio-mcp --doctor Check the connection to the app\n" +
|
|
58
|
+
" creavit-studio-mcp --list-tools Print tool schemas as JSON\n" +
|
|
59
|
+
" creavit-studio-mcp --version Print the version\n",
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
startRpcServer({ listTools, callTool });
|
|
63
|
+
log(`${SERVER_NAME} v${SERVER_VERSION} — ${(await listTools()).length} tools`);
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "creavit-studio-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that lets AI coding agents (Claude Code, Codex, Cursor) drive the Creavit Studio screen recording app",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"creavit-studio",
|
|
9
|
+
"screen-recording",
|
|
10
|
+
"claude",
|
|
11
|
+
"cursor",
|
|
12
|
+
"codex"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"creavit-studio-mcp": "bin/creavit-mcp.mjs"
|
|
18
|
+
},
|
|
19
|
+
"main": "./src/tools/index.mjs",
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"os": [
|
|
29
|
+
"darwin"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"yauzl": "^3.2.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"doctor": "node bin/creavit-mcp.mjs --doctor",
|
|
36
|
+
"tools": "node bin/creavit-mcp.mjs --list-tools",
|
|
37
|
+
"prepublishOnly": "node bin/creavit-mcp.mjs --list-tools > /dev/null"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// MCP sunucusu — çalışan uygulamaya bağlanan HTTP istemcisi.
|
|
2
|
+
// Endpoint dosyası her uygulama açılışında yenilendiği için her istekten önce
|
|
3
|
+
// tazelik kontrolü yapılır; böylece uygulama yeniden başlatıldığında MCP
|
|
4
|
+
// sunucusunu da yeniden başlatmak gerekmez.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { ENDPOINT_FILENAME, APP_NOT_RUNNING_HINT } from "./protocol.mjs";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ENDPOINT_DIRS = [
|
|
12
|
+
path.join(os.homedir(), "Library", "Application Support", "creavit-studio"),
|
|
13
|
+
path.join(os.homedir(), "Library", "Application Support", "Creavit Studio"),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export class BridgeUnavailableError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "BridgeUnavailableError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function endpointCandidates() {
|
|
24
|
+
const fromEnv = process.env.CREAVIT_AGENT_BRIDGE_ENDPOINT;
|
|
25
|
+
const dirs = fromEnv ? [path.dirname(fromEnv)] : DEFAULT_ENDPOINT_DIRS;
|
|
26
|
+
const files = fromEnv ? [fromEnv] : dirs.map((d) => path.join(d, ENDPOINT_FILENAME));
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readEndpoint() {
|
|
31
|
+
for (const file of endpointCandidates()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(file)) continue;
|
|
34
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
35
|
+
if (parsed?.port && parsed?.token) return { ...parsed, file };
|
|
36
|
+
} catch (_) {}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cached = null;
|
|
42
|
+
|
|
43
|
+
function currentEndpoint() {
|
|
44
|
+
const fresh = readEndpoint();
|
|
45
|
+
if (!fresh) {
|
|
46
|
+
cached = null;
|
|
47
|
+
throw new BridgeUnavailableError(APP_NOT_RUNNING_HINT);
|
|
48
|
+
}
|
|
49
|
+
// Port ya da token değiştiyse uygulama yeniden başlamış demektir.
|
|
50
|
+
if (!cached || cached.port !== fresh.port || cached.token !== fresh.token) {
|
|
51
|
+
cached = fresh;
|
|
52
|
+
}
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Köprüye istek at.
|
|
58
|
+
* @param {string} route "/rpc" | "/health" | "/commands" | "/events?since=1"
|
|
59
|
+
* @param {object} [options] { method, body, timeoutMs }
|
|
60
|
+
*/
|
|
61
|
+
async function request(route, { method = "GET", body, timeoutMs = 120_000 } = {}) {
|
|
62
|
+
const endpoint = currentEndpoint();
|
|
63
|
+
const url = `http://${endpoint.host || "127.0.0.1"}:${endpoint.port}${route}`;
|
|
64
|
+
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(url, {
|
|
71
|
+
method,
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${endpoint.token}`,
|
|
75
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
76
|
+
},
|
|
77
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
cached = null;
|
|
81
|
+
if (error?.name === "AbortError") {
|
|
82
|
+
throw new Error(`Bridge did not respond within ${timeoutMs}ms: ${route}`);
|
|
83
|
+
}
|
|
84
|
+
throw new BridgeUnavailableError(
|
|
85
|
+
`${APP_NOT_RUNNING_HINT}\n(connection error: ${error?.message || error})`,
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const text = await response.text();
|
|
92
|
+
let payload;
|
|
93
|
+
try {
|
|
94
|
+
payload = text ? JSON.parse(text) : {};
|
|
95
|
+
} catch (_) {
|
|
96
|
+
throw new Error(`Invalid response from bridge (${response.status}): ${text.slice(0, 200)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!response.ok || payload?.ok === false) {
|
|
100
|
+
const err = payload?.error || {};
|
|
101
|
+
const error = new Error(err.message || `Bridge error (${response.status})`);
|
|
102
|
+
error.code = err.code || String(response.status);
|
|
103
|
+
error.details = err.details || null;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return payload.result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function callCommand(command, params, timeoutMs) {
|
|
111
|
+
return request("/rpc", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: { command, params: params || {}, timeoutMs },
|
|
114
|
+
timeoutMs: timeoutMs || 120_000,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function health() {
|
|
119
|
+
return request("/health", { timeoutMs: 5_000 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function listBridgeCommands() {
|
|
123
|
+
return request("/commands", { timeoutMs: 10_000 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function isAppRunning() {
|
|
127
|
+
try {
|
|
128
|
+
await health();
|
|
129
|
+
return true;
|
|
130
|
+
} catch (_) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// MCP sunucusu — .crvt projelerini uygulama olmadan okuma.
|
|
2
|
+
// .crvt bir ZIP'tir; manifest.json'u doğrudan okuruz. Böylece proje listeleme
|
|
3
|
+
// ve inceleme araçları Creavit Studio kapalıyken de çalışır.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
// yauzl paketin kendi bağımlılığıdır; depo içinden çalışırken üstteki
|
|
13
|
+
// node_modules'tan da çözülür. Bulunamazsa salt-okunur araçlar köprüye düşer.
|
|
14
|
+
let yauzl = null;
|
|
15
|
+
try {
|
|
16
|
+
yauzl = require("yauzl");
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
|
|
19
|
+
export const canReadOffline = () => Boolean(yauzl);
|
|
20
|
+
|
|
21
|
+
export function defaultProjectsDir() {
|
|
22
|
+
return path.join(os.homedir(), "Downloads", "Creavit Studio");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readManifestOffline(zipPath) {
|
|
26
|
+
if (!yauzl) {
|
|
27
|
+
return Promise.reject(new Error("yauzl is not installed — offline reading is unavailable"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
32
|
+
if (err) return reject(err);
|
|
33
|
+
|
|
34
|
+
let found = false;
|
|
35
|
+
zipfile.on("entry", (entry) => {
|
|
36
|
+
if (entry.fileName !== "manifest.json") {
|
|
37
|
+
zipfile.readEntry();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
found = true;
|
|
41
|
+
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
42
|
+
if (streamErr) return reject(streamErr);
|
|
43
|
+
const chunks = [];
|
|
44
|
+
stream.on("data", (c) => chunks.push(c));
|
|
45
|
+
stream.on("end", () => {
|
|
46
|
+
try {
|
|
47
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
48
|
+
} catch (parseErr) {
|
|
49
|
+
reject(new Error(`Could not parse manifest.json: ${parseErr.message}`));
|
|
50
|
+
}
|
|
51
|
+
zipfile.close();
|
|
52
|
+
});
|
|
53
|
+
stream.on("error", reject);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
zipfile.on("end", () => {
|
|
57
|
+
if (!found) reject(new Error("manifest.json not found — not a valid .crvt file"));
|
|
58
|
+
});
|
|
59
|
+
zipfile.on("error", reject);
|
|
60
|
+
zipfile.readEntry();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function listProjectsOffline({ dir, limit } = {}) {
|
|
66
|
+
const root = dir || defaultProjectsDir();
|
|
67
|
+
if (!fs.existsSync(root)) return { dir: root, projects: [], source: "offline" };
|
|
68
|
+
|
|
69
|
+
const files = fs
|
|
70
|
+
.readdirSync(root)
|
|
71
|
+
.filter((f) => f.toLowerCase().endsWith(".crvt"))
|
|
72
|
+
.map((f) => {
|
|
73
|
+
const filePath = path.join(root, f);
|
|
74
|
+
const stat = fs.statSync(filePath);
|
|
75
|
+
return {
|
|
76
|
+
fileName: f,
|
|
77
|
+
filePath,
|
|
78
|
+
sizeBytes: stat.size,
|
|
79
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
80
|
+
};
|
|
81
|
+
})
|
|
82
|
+
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt))
|
|
83
|
+
.slice(0, Number(limit) > 0 ? Number(limit) : 100);
|
|
84
|
+
|
|
85
|
+
const projects = await Promise.all(
|
|
86
|
+
files.map(async (file) => {
|
|
87
|
+
try {
|
|
88
|
+
const manifest = await readManifestOffline(file.filePath);
|
|
89
|
+
// Manifest v2'de süre klip başına tutulur.
|
|
90
|
+
const durationMs =
|
|
91
|
+
(manifest?.clips || []).reduce((acc, c) => acc + (Number(c?.durationMs) || 0), 0) ||
|
|
92
|
+
null;
|
|
93
|
+
return {
|
|
94
|
+
...file,
|
|
95
|
+
name: manifest?.name || file.fileName.replace(/\.crvt$/i, ""),
|
|
96
|
+
durationMs,
|
|
97
|
+
durationSeconds: durationMs ? Number((durationMs / 1000).toFixed(2)) : null,
|
|
98
|
+
createdAt: manifest?.createdAt ?? null,
|
|
99
|
+
clipCount: (manifest?.clips || []).length,
|
|
100
|
+
};
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return { ...file, name: file.fileName.replace(/\.crvt$/i, ""), manifestError: true };
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { dir: root, projects, source: "offline" };
|
|
108
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// .crvt manifest özeti (v2 şeması).
|
|
2
|
+
//
|
|
3
|
+
// Gerçek manifest şekli:
|
|
4
|
+
// { version, createdAt, name, projectSettings:{resolution,fps},
|
|
5
|
+
// playerSettings:{...}, clips:[{id,name,durationMs,media,segments,mouse}] }
|
|
6
|
+
//
|
|
7
|
+
// İki varyant var: taze kayıttan çıkan "hafif" manifest (playerSettings'te
|
|
8
|
+
// sadece birkaç anahtar) ve editörde kaydedilmiş "tam" manifest. Özet ikisini
|
|
9
|
+
// de aynı biçimde raporlar, eksik alanlar null kalır.
|
|
10
|
+
//
|
|
11
|
+
// Bu dosya MCP paketinin tek özet kaynağıdır — Electron köprüsü ham manifest
|
|
12
|
+
// döndürür, özetleme daima burada yapılır. Böylece mcp/ tek başına dağıtılabilir.
|
|
13
|
+
|
|
14
|
+
function sum(list, pick) {
|
|
15
|
+
return (list || []).reduce((acc, item) => acc + (Number(pick(item)) || 0), 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function summarizeManifest(manifest, filePath) {
|
|
19
|
+
const ps = manifest?.playerSettings || {};
|
|
20
|
+
const clips = Array.isArray(manifest?.clips) ? manifest.clips : [];
|
|
21
|
+
const totalDurationMs = sum(clips, (c) => c.durationMs);
|
|
22
|
+
|
|
23
|
+
// Editörde kaydedilmiş manifest'te tüm ayar anahtarları bulunur; hafif
|
|
24
|
+
// manifest'te yoktur. Ajanın hangi durumda olduğunu bilmesi gerekiyor.
|
|
25
|
+
const isEdited = "zoomRanges" in ps || "cameraSettings" in ps;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
filePath,
|
|
29
|
+
name: manifest?.name || null,
|
|
30
|
+
manifestVersion: manifest?.version ?? null,
|
|
31
|
+
createdAt: manifest?.createdAt || null,
|
|
32
|
+
edited: isEdited,
|
|
33
|
+
durationMs: totalDurationMs || null,
|
|
34
|
+
durationSeconds: totalDurationMs ? Number((totalDurationMs / 1000).toFixed(2)) : null,
|
|
35
|
+
output: {
|
|
36
|
+
resolution: manifest?.projectSettings?.resolution || ps.canvasSize || null,
|
|
37
|
+
fps: manifest?.projectSettings?.fps ?? null,
|
|
38
|
+
cropRatio: ps.cropRatio ?? null,
|
|
39
|
+
},
|
|
40
|
+
look: {
|
|
41
|
+
backgroundType: ps.backgroundType ?? null,
|
|
42
|
+
backgroundColor: ps.backgroundColor ?? null,
|
|
43
|
+
backgroundImage: ps.backgroundImage || null,
|
|
44
|
+
backgroundBlur: ps.backgroundBlur ?? null,
|
|
45
|
+
padding: ps.padding ?? ps.basePadding ?? null,
|
|
46
|
+
radius: ps.radius ?? ps.baseRadius ?? null,
|
|
47
|
+
shadowSize: ps.shadowSize ?? ps.baseShadowSize ?? null,
|
|
48
|
+
},
|
|
49
|
+
clips: clips.map((clip) => ({
|
|
50
|
+
id: clip.id ?? null,
|
|
51
|
+
name: clip.name ?? null,
|
|
52
|
+
durationMs: clip.durationMs ?? null,
|
|
53
|
+
segmentCount: (clip.segments || []).length,
|
|
54
|
+
hasMouseData: Boolean(clip.mouse),
|
|
55
|
+
media: clip.media
|
|
56
|
+
? Object.fromEntries(
|
|
57
|
+
Object.entries(clip.media).map(([k, v]) => [
|
|
58
|
+
k,
|
|
59
|
+
typeof v === "string" ? v : v?.entry || v?.path || true,
|
|
60
|
+
]),
|
|
61
|
+
)
|
|
62
|
+
: null,
|
|
63
|
+
})),
|
|
64
|
+
counts: {
|
|
65
|
+
clips: clips.length,
|
|
66
|
+
segments: sum(clips, (c) => (c.segments || []).length),
|
|
67
|
+
zoomRanges: (ps.zoomRanges || []).length,
|
|
68
|
+
animation3DRanges: (ps.animation3DRanges || []).length,
|
|
69
|
+
codeSnippets: (ps.codeSnippets || []).length,
|
|
70
|
+
stepSegments: (ps.stepSegments || []).length,
|
|
71
|
+
layoutRanges: (manifest?.layoutRanges || []).length,
|
|
72
|
+
titleCards: (manifest?.titleCards || []).length,
|
|
73
|
+
soundEffectSegments: (manifest?.soundEffectSegments || []).length,
|
|
74
|
+
shortcutSegments: (manifest?.shortcutSegments || []).length,
|
|
75
|
+
},
|
|
76
|
+
effects: {
|
|
77
|
+
camera: ps.cameraSettings ? Boolean(ps.cameraSettings.visible) : null,
|
|
78
|
+
crtMonitor: ps.crtMonitorEffectEnabled ?? null,
|
|
79
|
+
cursorAnimation: ps.cursorAnimationEnabled ?? null,
|
|
80
|
+
velocityBlur: ps.velocityBlurEnabled ?? null,
|
|
81
|
+
ultrathink: ps.ultrathinkEnabled ?? null,
|
|
82
|
+
cursorTheme: ps.selectedCursorTheme ?? null,
|
|
83
|
+
},
|
|
84
|
+
note: isEdited
|
|
85
|
+
? "This project has been saved from the editor — all player settings are present in the manifest."
|
|
86
|
+
: "This is a fresh recording: only basic settings exist. Opening it in the editor and saving writes the full settings.",
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/protocol.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// MCP sunucusu — köprü protokolü sabitleri.
|
|
2
|
+
// electron/agentBridge/protocol.cjs ile eşleşmeli.
|
|
3
|
+
|
|
4
|
+
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
5
|
+
export const ENDPOINT_FILENAME = "agent-bridge.json";
|
|
6
|
+
|
|
7
|
+
export const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
8
|
+
export const SERVER_NAME = "creavit-studio";
|
|
9
|
+
export const SERVER_VERSION = "1.0.0";
|
|
10
|
+
|
|
11
|
+
export const JSONRPC_ERRORS = {
|
|
12
|
+
PARSE_ERROR: -32700,
|
|
13
|
+
INVALID_REQUEST: -32600,
|
|
14
|
+
METHOD_NOT_FOUND: -32601,
|
|
15
|
+
INVALID_PARAMS: -32602,
|
|
16
|
+
INTERNAL_ERROR: -32603,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Uygulama kapalıyken ajana ne yapması gerektiğini net söyle.
|
|
20
|
+
export const APP_NOT_RUNNING_HINT =
|
|
21
|
+
"Creavit Studio is not running, or its agent bridge is disabled. Start the app " +
|
|
22
|
+
"(Creavit Studio.app, or `npm run electron:dev` in the repo). The bridge may also " +
|
|
23
|
+
"have been turned off with CREAVIT_AGENT_BRIDGE=0. Tools that work without the app: " +
|
|
24
|
+
"creavit_project_list, creavit_project_summary, creavit_project_manifest.";
|