Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,50 @@ approve it in OpenChatCut before reporting that the edit was applied.

External agents invoke the same internal editing tools and `EditorCore` commands as the editor itself. There are no separate project formats that can drift apart, and the live timeline is not changed while an external draft is being prepared.

### Qoder

Qoder is one-click too: the guide's **Connect** button writes the endpoint and
bearer token into Qoder's own settings file, keeping any unrelated settings and
MCP servers already there.

```text
~/.qoder/settings.json (international build)
~/.qoder-cn/settings.json (China build — written as well when installed)
```

```json
{
"mcpServers": {
"openchatcut": {
"type": "http",
"url": "http://localhost:5199/api/external-mcp/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
```

Restart Qoder afterwards to load the server.

### Qwen Work (千问办公)

千问办公 keeps custom MCP servers inside its own connector store rather than a
config file, so OpenChatCut cannot write this one for you. In the guide, use
**Copy config** on the 千问办公 card, then in 千问办公 open
连接器 → 自定义 MCP → 添加, paste the JSON, and save:

```json
{
"mcpServers": {
"openchatcut": {
"type": "streamable-http",
"url": "http://localhost:5199/api/external-mcp/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
```

### Protecting MCP access

When exposing the MCP endpoint yourself, configure:
Expand Down
42 changes: 42 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,48 @@ claude mcp add --transport http openchatcut \

外部 Agent 调用的仍是编辑器内部同一套工具和 `EditorCore` 命令,不存在两套互相漂移的工程格式;外部草稿准备期间不会修改正式时间线。

### Qoder

Qoder 同样支持一键接入:指南里的「连接」按钮会把端点和 Bearer 令牌写进 Qoder
自己的设置文件,已有的其它设置和 MCP 服务保持不变。

```text
~/.qoder/settings.json (国际版)
~/.qoder-cn/settings.json (国内版;已安装时一并写入)
```

```json
{
"mcpServers": {
"openchatcut": {
"type": "http",
"url": "http://localhost:5199/api/external-mcp/mcp",
"headers": { "Authorization": "Bearer <令牌>" }
}
}
}
```

写入后重启 Qoder 生效。

### 千问办公

千问办公 的自定义 MCP 保存在应用内部的连接器里,没有可供写入的配置文件,
因此这一步无法一键完成:在指南中点「千问办公」卡片的「复制配置」,再到
千问办公 的 连接器 → 自定义 MCP → 添加 中粘贴并保存:

```json
{
"mcpServers": {
"openchatcut": {
"type": "streamable-http",
"url": "http://localhost:5199/api/external-mcp/mcp",
"headers": { "Authorization": "Bearer <令牌>" }
}
}
}
```

### MCP 访问保护

自行暴露 MCP 入口时可配置:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"verify:editor": "tsx src/editor/storeCommandBuilder.verify.ts && tsx src/editor/keyframes.verify.ts && tsx src/editor/flexCrop.verify.ts && tsx src/editor/multiSelect.verify.ts && tsx src/editor/professionalTimeline.verify.ts && tsx src/editor/proposal.verify.ts && tsx src/editor/ripple-fade.verify.ts && tsx src/editor/ripple.verify.ts && tsx src/editor/split.verify.ts && tsx src/editor/transitionPreview.verify.ts && tsx src/editor/captionReferences.verify.ts && tsx src/editor/zoom.verify.ts && tsx src/editor/keyframes-check.verify.ts",
"verify:captions": "tsx src/captions/captionCueMenu.verify.ts && tsx src/captions/captionIdentity.verify.ts && tsx src/captions/exportCaptions.verify.ts && tsx src/captions/presetStore.verify.ts && tsx src/captions/renderStyles.verify.ts && tsx src/captions/segmenter.verify.ts && tsx src/captions/styles.verify.ts && tsx src/transcript/edit.verify.ts && tsx src/transcript/transcribe-jobs.verify.ts && tsx src/transcript/variants.verify.ts && tsx src/audio/loudness.verify.ts && tsx src/audio/vad.verify.ts && tsx src/transcript/transcribe-jobs-check.verify.ts && tsx src/audio/loudnessBatch.verify.ts && tsx src/transcript/projection.verify.ts",
"verify:agent-tools": "tsx src/agent/external-edit-session-apply.verify.ts && tsx src/agent/external-edit-session-cancellation.verify.ts && tsx src/agent/external-edit-session-core.verify.ts && tsx src/agent/external-edit-session-projection.verify.ts && tsx src/agent/external-edit-session-runtime.verify.ts && tsx src/agent/external-tool-policy.verify.ts && tsx src/agent/progress/job-model.verify.ts && tsx src/agent/progress/track-progress-targets.verify.ts && tsx src/agent/selection-refs.verify.ts && tsx src/agent/skills/plugin-skills.verify.ts && tsx src/agent/skills/skills-catalog.verify.ts && tsx src/agent/tools/agent-gap-p3.verify.ts && tsx src/agent/tools/audio-assets.verify.ts && tsx src/agent/tools/auto-grade-tools.verify.ts && tsx src/agent/tools/captions-lanes.verify.ts && tsx src/agent/tools/captions-tools.verify.ts && tsx src/agent/tools/design-tools.verify.ts && tsx src/agent/tools/edit-asset-tools.verify.ts && tsx src/agent/tools/edit-item-authored.verify.ts && tsx src/agent/tools/edit-item-generic.verify.ts && tsx src/agent/tools/edit-item-source-range.verify.ts && tsx src/agent/tools/edit-item-pool-replacement.verify.ts && tsx src/agent/tools/timeline-import-tools.verify.ts && tsx src/agent/tools/edit-item-volume-hint.verify.ts && tsx src/agent/tools/export-tools.verify.ts && tsx src/agent/tools/followup-tools.verify.ts && tsx src/agent/tools/font-tools.verify.ts && tsx src/agent/tools/frames-tool.verify.ts && tsx src/agent/tools/renderSnapshotMedia.verify.ts && tsx src/agent/tools/friction-tools.verify.ts && tsx src/agent/tools/generate-rerun.verify.ts && tsx src/agent/tools/highlight-tool.verify.ts && tsx src/agent/tools/markers-tools.verify.ts && tsx src/agent/tools/media-pool-favorite-delete.verify.ts && tsx src/agent/tools/media-pool-relink.verify.ts && tsx src/agent/tools/media-pool-tools.verify.ts && tsx src/agent/tools/mg-code-tools.verify.ts && tsx src/agent/tools/mg-video-tools.verify.ts && tsx src/agent/tools/probe-tools.verify.ts && tsx src/agent/toolFailureNote.verify.ts && tsx src/agent/tools/project-tools.verify.ts && tsx src/agent/tools/followup-tools-check.verify.ts && tsx src/agent/tools/probe-tools-check.verify.ts && tsx src/agent/tools/read-project-tools-check.verify.ts && tsx src/agent/tools/stock-tools-check.verify.ts && tsx src/agent/tools/stock-tools-probe.verify.ts && tsx src/agent/tools/template-tools-check.verify.ts && node scripts/run-check.mjs src/agent/tools/effect-tools.verify.ts && node scripts/run-check.mjs src/agent/tools/library-edit-item.verify.ts && tsx src/agent/tools/read-project-tools.verify.ts && tsx src/agent/tools/shader-tools.verify.ts && tsx src/agent/tools/skill-tools.verify.ts && tsx src/agent/tools/transcript-clear-edits.verify.ts && tsx src/agent/tools/transcript-tools.verify.ts && tsx src/agent/tools/version-tools.verify.ts && tsx src/agent/tools/web-tools.verify.ts",
"verify:components": "tsx --tsconfig tsconfig.app.json src/components/SafeZoneOverlay.verify.tsx && tsx --tsconfig tsconfig.app.json src/components/chat/chatPopoverGeometry.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/chatScrollNavigation.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/chatTextSelection.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/editorDragReference.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/message-groups.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/template-reference-localization.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/workflow-starters.verify.tsx && tsx src/components/chat/capabilityBanner.verify.ts && tsx src/components/chat/chatDocumentParse.verify.ts && tsx --tsconfig tsconfig.app.json src/components/inspector/inspectorPropSchemaLayout.verify.ts && tsx --tsconfig tsconfig.app.json src/components/inspector/inspectorTextArea.verify.ts && tsx src/components/inspector/inspector-controls.verify.ts && tsx --tsconfig tsconfig.app.json src/components/preview/preview-transform-overlay.verify.tsx && tsx --tsconfig tsconfig.app.json src/components/preview/previewTextEdit.verify.ts && tsx --tsconfig tsconfig.app.json src/components/preview/timeline-hover-preview.verify.ts && tsx --tsconfig tsconfig.app.json src/components/previewAudioPool.verify.ts && tsx src/components/settings/settingsStorage.verify.ts && tsx src/components/settings/storageMigration.verify.ts && tsx --tsconfig tsconfig.app.json src/components/settings/design-style-localization.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/clipContextSelection.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/timelineChatDrop.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/timelineWindow.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/trackContextOperations.verify.ts && tsx src/library/extension-installed.verify.ts && tsx src/library/library-card-grid.verify.ts && tsx src/library/resource-tabs-drag.verify.ts && tsx src/library/template-card-actions.verify.ts && tsx src/library/template-category-sticky.verify.ts && tsx src/components/chat/message-groups-check.verify.ts && tsx src/components/chat/widget-parse-check.verify.ts && tsx src/library/sequenceOptions.verify.ts",
"verify:components": "tsx --tsconfig tsconfig.app.json src/components/SafeZoneOverlay.verify.tsx && tsx --tsconfig tsconfig.app.json src/components/chat/chatPopoverGeometry.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/chatScrollNavigation.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/chatTextSelection.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/editorDragReference.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/message-groups.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/template-reference-localization.verify.ts && tsx --tsconfig tsconfig.app.json src/components/chat/workflow-starters.verify.tsx && tsx src/components/chat/capabilityBanner.verify.ts && tsx src/components/chat/chatDocumentParse.verify.ts && tsx --tsconfig tsconfig.app.json src/components/inspector/inspectorPropSchemaLayout.verify.ts && tsx --tsconfig tsconfig.app.json src/components/inspector/inspectorTextArea.verify.ts && tsx src/components/inspector/inspector-controls.verify.ts && tsx --tsconfig tsconfig.app.json src/components/preview/preview-transform-overlay.verify.tsx && tsx --tsconfig tsconfig.app.json src/components/preview/previewTextEdit.verify.ts && tsx --tsconfig tsconfig.app.json src/components/preview/timeline-hover-preview.verify.ts && tsx --tsconfig tsconfig.app.json src/components/previewAudioPool.verify.ts && tsx src/components/settings/settingsStorage.verify.ts && tsx src/components/settings/storageMigration.verify.ts && tsx --tsconfig tsconfig.app.json src/components/settings/design-style-localization.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/clipContextSelection.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/timelineChatDrop.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/timelineWindow.verify.ts && tsx --tsconfig tsconfig.app.json src/components/timeline/trackContextOperations.verify.ts && tsx src/library/extension-installed.verify.ts && tsx src/library/library-card-grid.verify.ts && tsx src/library/resource-tabs-drag.verify.ts && tsx src/library/template-card-actions.verify.ts && tsx src/library/template-category-sticky.verify.ts && tsx src/components/chat/message-groups-check.verify.ts && tsx src/components/chat/widget-parse-check.verify.ts && tsx src/library/sequenceOptions.verify.ts && tsx src/components/settings/mcpClientConfig.verify.ts",
"verify:media-persist": "tsx --tsconfig tsconfig.app.json src/media/add-solid-canvas-card.verify.tsx && tsx src/media/asset-menu-destinations.verify.tsx && tsx src/media/assetExport.verify.ts && tsx src/media/media-aspect-badge.verify.ts && tsx src/media/media-favorites-folder.verify.ts && tsx src/media/media-folder-drop.verify.ts && tsx src/media/media-pool-progressive-import.verify.ts && tsx src/media/media-view-toggle.verify.ts && tsx src/media/mediaFolderDrop.verify.ts && tsx src/media/mediaMarquee.verify.ts && tsx src/media/mediaPoolShortcutScope.verify.ts && tsx src/media/mediaSelectionActions.verify.ts && tsx src/media/mgPreview.verify.ts && tsx src/media/searchMedia.verify.ts && tsx src/media/semantic-search/mediaFrames.verify.ts && tsx src/media/upload-session-recovery.verify.ts && tsx src/persist/agentRuntimeTransfer.verify.ts && tsx src/persist/chat-persist.verify.ts && tsx src/persist/exportHistoryStore.verify.ts && tsx src/persist/jobRegistryStore.verify.ts && tsx src/persist/idbConnectionRecovery.verify.ts && tsx src/persist/mediaBlobStore.verify.ts && tsx src/persist/mediaCleanup.verify.ts && tsx src/persist/projectStore.verify.ts && tsx src/persist/projectTransfer.verify.ts && tsx src/persist/proposalStore.verify.ts && tsx src/persist/sessionPrefs.verify.ts && tsx src/persist/skillStore.verify.ts && tsx src/persist/template-recent.verify.ts && tsx src/persist/version-store.verify.ts && tsx src/persist/jobRegistryStore-check.verify.ts && tsx src/persist/mediaBlobStore-check.verify.ts && tsx src/persist/projectStore-check.verify.ts && tsx src/persist/projectTransfer-check.verify.ts",
"verify:export-gl": "tsx src/export/export-reliability.verify.ts && tsx src/export/exportMediaPlan.verify.ts && tsx src/export/range.verify.ts && tsx src/gl/customTransitions.verify.ts && tsx src/gl/fx/chroma-key.verify.ts && tsx src/gl/fx/cube.verify.ts && tsx src/gl/fx/fx.verify.ts && tsx src/gl/shader-contract.verify.ts && tsx src/generate/media-export.geometry.verify.ts && tsx src/generate/video.verify.ts && tsx src/plugins/export.verify.ts && tsx src/plugins/resourcePreview.verify.ts && tsx src/plugins/validate.verify.ts && tsx src/multicam/align.verify.ts && tsx src/multicam/sync.verify.ts && tsx src/reframe/detect.verify.ts && tsx src/script/script.verify.ts && tsx --tsconfig tsconfig.app.json src/fonts/notoSansOffline.verify.ts",
"verify:server-extra": "tsx server/media-process.verify.ts && tsx server/media-references.verify.ts && tsx server/media-normalization-reason.verify.ts && tsx server/media-normalization-reference.verify.ts && tsx server/media-normalization-admission.verify.ts && tsx server/plugins/upload-multipart-meta.verify.ts && tsx server/plugins/firecrawl.verify.ts && tsx server/plugins/request-shape-gate.verify.ts && tsx server/plugins/isolate-voice.verify.ts && tsx server/plugins/project-store-merge.verify.ts && tsx server/plugins/render-clip-cancellation.verify.ts && tsx server/plugins/storage-lifecycle.verify.ts && tsx server/plugins/video-reference.verify.ts && tsx server/safe-public-fetch.verify.ts && tsx shared/timeline-geometry.verify.ts && tsx remotion/render-timeout.verify.mjs && tsx server/plugins/stock-check.verify.ts && tsx server/media-stream.verify.ts",
Expand Down
72 changes: 47 additions & 25 deletions server/external-agent/client-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
// are merged atomically (write-to-temp + rename) and never clobbered when the
// existing content fails to parse.
import { spawn } from 'node:child_process';
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import path from 'node:path';

export const CONNECT_CLIENTS = ['claude', 'codex', 'cursor', 'antigravity'] as const;
export const CONNECT_CLIENTS = ['claude', 'codex', 'cursor', 'antigravity', 'qoder'] as const;
export type ConnectClient = (typeof CONNECT_CLIENTS)[number];
/** Clients whose MCP servers live in a mergeable JSON config file. Codex is
* driven through its own CLI and keeps a TOML config we do not parse. */
export type JsonClient = Exclude<ConnectClient, 'codex'>;

export type ClientConnectResult =
| { ok: true; paths: string[] }
Expand Down Expand Up @@ -76,17 +79,40 @@ function httpEntry(endpoint: string, token: string): Record<string, unknown> {
return { type: 'http', url: endpoint, headers: { Authorization: `Bearer ${token}` } };
}

async function connectJsonClient(
file: string,
async function connectJsonClients(
files: readonly string[],
entry: Record<string, unknown>,
): Promise<{ ok: true; paths: [string] } | MergeFailure> {
const result = await mergeJsonConfig(file, (root) => {
const servers = (root.mcpServers as Record<string, unknown> | undefined) ?? {};
servers.openchatcut = entry;
root.mcpServers = servers;
});
if (!result.ok) return result;
return { ok: true, paths: [file] };
): Promise<{ ok: true; paths: string[] } | MergeFailure> {
const written: string[] = [];
for (const file of files) {
const result = await mergeJsonConfig(file, (root) => {
const servers = (root.mcpServers as Record<string, unknown> | undefined) ?? {};
servers.openchatcut = entry;
root.mcpServers = servers;
});
if (!result.ok) return result;
written.push(file);
}
return { ok: true, paths: written };
}

/** Config files this client reads. Qoder ships an international and a China
* build that keep the same settings file under `.qoder` or `.qoder-cn`, and
* only the installed one is meaningful, so an absent sibling is skipped; when
* neither exists yet the documented `.qoder` path is created. */
async function jsonClientFiles(client: JsonClient, baseDir: string): Promise<string[]> {
if (client === 'claude') return [path.join(baseDir, '.claude.json')];
if (client === 'cursor') return [path.join(baseDir, '.cursor', 'mcp.json')];
if (client === 'antigravity') return [path.join(baseDir, '.gemini', 'antigravity', 'mcp_config.json')];
const homes: string[] = [];
for (const dir of ['.qoder', '.qoder-cn']) {
try {
if ((await stat(path.join(baseDir, dir))).isDirectory()) homes.push(dir);
} catch {
/* that build is not installed */
}
}
return (homes.length ? homes : ['.qoder']).map((dir) => path.join(baseDir, dir, 'settings.json'));
}

function runCodex(bin: string, endpoint: string, env: NodeJS.ProcessEnv): Promise<{ code: number | null; stderr: string }> {
Expand Down Expand Up @@ -151,28 +177,24 @@ export async function connectExternalClient(
token: string,
options: ClientConnectOptions = {},
): Promise<ClientConnectResult> {
if (!CONNECT_CLIENTS.includes(client as ConnectClient)) {
const selected = client as ConnectClient;
if (!CONNECT_CLIENTS.includes(selected)) {
return { ok: false, error: 'invalid-client' };
}
if (typeof endpoint !== 'string' || !/^https?:\/\//.test(endpoint) || !TOKEN_PATTERN.test(token)) {
return { ok: false, error: 'invalid-token' };
}
const baseDir = options.baseDir ?? homedir();
if (client === 'codex') {
if (selected === 'codex') {
const result = await connectCodex(endpoint, token, baseDir, options.codexBin);
if (!result.ok) return result;
return { ok: true, paths: result.paths.map((file) => displayPath(baseDir, file)) };
}
const files: Record<'claude' | 'cursor' | 'antigravity', { file: string; entry: Record<string, unknown> }> = {
claude: { file: path.join(baseDir, '.claude.json'), entry: httpEntry(endpoint, token) },
cursor: { file: path.join(baseDir, '.cursor', 'mcp.json'), entry: httpEntry(endpoint, token) },
antigravity: {
file: path.join(baseDir, '.gemini', 'antigravity', 'mcp_config.json'),
entry: { httpUrl: endpoint, headers: { Authorization: `Bearer ${token}` } },
},
};
const target = files[client as 'claude' | 'cursor' | 'antigravity'];
const result = await connectJsonClient(target.file, target.entry);
const files = await jsonClientFiles(selected, baseDir);
const entry = selected === 'antigravity'
? { httpUrl: endpoint, headers: { Authorization: `Bearer ${token}` } }
: httpEntry(endpoint, token);
const result = await connectJsonClients(files, entry);
if (!result.ok) return result;
return { ok: true, paths: [displayPath(baseDir, target.file)] };
return { ok: true, paths: result.paths.map((file) => displayPath(baseDir, file)) };
}
Loading