@m2canvas/connect 0.9.0 → 0.10.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 +5 -5
- package/dist/bridge/release-manifest.js +4 -3
- package/dist/bridge/security.js +7 -6
- package/dist/bridge/uninstall.js +1 -1
- package/dist/bridge/update.js +2 -2
- package/dist/clients.js +16 -4
- package/dist/config-files.js +48 -5
- package/dist/site-config.js +28 -0
- package/install/uninstall.sh +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @m2canvas/connect
|
|
2
2
|
|
|
3
|
-
Connect the agent clients on your machine to [M2 Canvas](https://
|
|
3
|
+
Connect the agent clients on your machine to [M2 Canvas](https://patch.design) in one command.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @m2canvas/connect
|
|
@@ -34,7 +34,7 @@ npx -y @m2canvas/connect@latest bridge
|
|
|
34
34
|
|
|
35
35
|
Lets M2 (in Chrome or another Chromium browser) start and talk to the agents on this machine over [ACP](https://agentclientprotocol.com): Claude Code, Codex, Cursor CLI, OpenCode and Qoder. Run it in the project directory you want the agents to work in, and keep the window open. Copy the command from M2's Local Agent tab: it adds `--pair <value>` so that page pairs by itself.
|
|
36
36
|
|
|
37
|
-
- **Local only.** It listens on `127.0.0.1:47821` and only accepts pages from `https://
|
|
37
|
+
- **Local only.** It listens on `127.0.0.1:47821` and only accepts pages from `https://patch.design`. Other sites, other machines and other hosts are refused before a connection is made. (`--dev` also accepts a local M2 dev server on port 5173; any app you serve on that port could then ask to pair, so only use it while developing M2.)
|
|
38
38
|
- **Pairing gives the page your agents.** A paired page can make these agents read and change files and run commands as you, the same as typing into them yourself. Only run commands you copied from M2, and only enter the code into M2.
|
|
39
39
|
- **Pairing.** When you start the bridge with the command M2 gives you, the page that gave it pairs automatically within 10 minutes. The command only carries a hash (`--pair`); the secret it matches stays in that browser tab, so the command is safe to appear in your shell history, and it works once. Any other page asks for the pairing code printed in this window; the window also shows which site is asking. After that the page remembers the pairing for 30 days of inactivity. After too many wrong codes, new pages can't pair until you restart the bridge (pages already paired keep working); close the page the window names first. `bridge --unpair` forgets all paired pages. If you can't find this window, run the command M2 shows under the code box: it adds `--replace`, which stops the M2 bridge already running on the port (its agent sessions end) and starts a new one that pairs by itself. It only stops a bridge (0.6.0 or later) it can recognize from a small state file in `~/.m2canvas` (process id and start time; no codes or tokens are written there), never anything else on the port, and not on Windows. When it can't recognize what holds the port, it shows you that process (or how to find it) and leaves it alone. With an older bridge, M2 shows the Ctrl+C instructions instead.
|
|
40
40
|
- **Your own logins.** Agents use their own sign-in and their own M2 MCP setup. It starts agents with a minimal environment, so variables set by a host app (for example `ANTHROPIC_BASE_URL` inside Claude Code) don't leak into them.
|
|
@@ -52,14 +52,14 @@ Options: `--port <n>`, `--cwd <dir>` (default: current directory), `--verbose` (
|
|
|
52
52
|
M2's Local Agent tab can have an agent app set the bridge up for you, so you need neither a terminal nor Node.js:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
curl -fsSL https://
|
|
55
|
+
curl -fsSL https://patch.design/connect/install.sh | sh -s -- --pair <value from M2>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
- **From M2.** Click "Connect with Claude" (or Codex, or Cursor): the app opens with this command in a prompt, and you press Return. The installer puts a private Node.js and a pinned version of this package in `~/.m2canvas`, checking every download against the hashes M2 publishes; writes the launcher `~/.m2canvas/bin/m2-bridge`; and starts the bridge in the background. The page pairs by itself. Agent sessions start in `~/.m2canvas/workspace`.
|
|
59
|
-
- **Nothing starts at login.** After a restart, M2 asks the agent app to run `~/.m2canvas/bin/m2-bridge start --pair <value>` (you can run it yourself too). The launcher first updates the bridge to the version listed at `https://
|
|
59
|
+
- **Nothing starts at login.** After a restart, M2 asks the agent app to run `~/.m2canvas/bin/m2-bridge start --pair <value>` (you can run it yourself too). The launcher first updates the bridge to the version listed at `https://patch.design/connect/manifest.json`, and goes back to the previous version if the new one doesn't start. The bridge keeps running after you quit the app, until you log out or shut down.
|
|
60
60
|
- **No pairing codes in the background.** A bridge started this way has no window to show a code in. When a page isn't paired yet (another browser, or the pairing expired), M2 gives it the same start command: the new bridge process sees the one already running and hands the page's pairing value to it, so the page pairs without restarting the bridge and other tabs keep their sessions. The handoff goes through `~/.m2canvas/bridge-<port>.sock` plus a one-time file in `~/.m2canvas`, so only a process that can write to that folder (your account; agent sandboxes normally can't write there) can hand a pairing value over; the file holds only the hash and is deleted right away. If that doesn't work, M2 offers to restart the bridge with `--replace`, which ends the sessions on the old one.
|
|
61
61
|
- **Sandboxes.** Codex needs its Full access permission mode: its default sandbox can't reach the network or write to your home folder. Inside the Codex or Claude command sandbox, the scripts change nothing and say what to do instead.
|
|
62
62
|
- **What it leaves alone.** macOS 13.5 or later. It doesn't use sudo, register anything to start at login, change your shell config or PATH, or touch a Node.js you installed yourself. Logs are in `~/.m2canvas/logs/`.
|
|
63
|
-
- **Uninstall** with "Uninstall local connection" in the Local Agent tab's ⋯ menu, or `curl -fsSL https://
|
|
63
|
+
- **Uninstall** with "Uninstall local connection" in the Local Agent tab's ⋯ menu, or `curl -fsSL https://patch.design/connect/uninstall.sh | sh`. It stops the bridge and removes what the installer added; `~/.m2canvas/workspace` is kept if it has files in it, and your agents' M2 settings stay as they are. While a bridge started this way (0.8.0 or later) is running, M2 asks it to uninstall itself: it stops its agents, runs the same uninstaller (shipped in this package as `install/uninstall.sh`), reports what happened to M2, and exits. Only a paired page can ask, and it can't pass anything that changes what gets removed. Otherwise M2 offers the M2 app, an agent app or the Terminal command.
|
|
64
64
|
|
|
65
65
|
The `npx` command above still works and is what M2 shows under "Advanced", for running the bridge in a folder you choose.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 主站更新清单(`MANIFEST_URL`,主站的 `/connect/manifest.json`,地址在 `site-config.ts`)的格式、解析与下载地址
|
|
3
|
+
* (change acp-bridge-one-time-install D13 / D14 / R7)。
|
|
3
4
|
*
|
|
4
5
|
* 清单由仓库的 `packages/connect/install/release.json` 生成,随 App 部署发布;部署 App 才是放量开关,发 npm 不等于推给所有用户。
|
|
5
6
|
* 清单**只提供版本号与哈希**:下载地址在这里由版本号拼出,固定指向官方源与 npmmirror。
|
|
6
|
-
* 不要在清单里加 URL 字段 ——
|
|
7
|
+
* 不要在清单里加 URL 字段 —— 那会让清单(或篡改清单的人)把下载指到任意地方,信任根就不止主站加哈希了。
|
|
7
8
|
*
|
|
8
9
|
* 跨版本兼容:已安装的旧版本 bridge 会读新部署的清单(`--check-update` 跑的是当前版本的代码)。
|
|
9
10
|
* - 同一 `schema` 内只加字段;本解析器忽略不认识的字段。
|
|
10
11
|
* - 要改语义就升 `schema`;旧版本认不出新 schema 时按「读清单失败」处理,用当前版本继续(不会误装)。
|
|
11
12
|
* - 版本号与哈希一律严格校验格式:它们会进入文件路径与下载地址。
|
|
12
13
|
*/
|
|
13
|
-
export
|
|
14
|
+
export { MANIFEST_URL } from '../site-config.js';
|
|
14
15
|
export const MANIFEST_SCHEMA = 1;
|
|
15
16
|
/** 只认纯 x.y.z(不接受预发布、构建元数据):版本号会拼进目录名与下载地址 */
|
|
16
17
|
const VERSION_RE = /^\d{1,4}\.\d{1,4}\.\d{1,6}$/;
|
package/dist/bridge/security.js
CHANGED
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
* 连上并配对之后页面能做什么,见 server.ts / acp-guard.ts 的文件头:配对 = 授权该页面驱动本机 agent。
|
|
8
8
|
*
|
|
9
9
|
* 维护约束:
|
|
10
|
-
* - Origin 必须精确匹配(scheme + host + port
|
|
11
|
-
* `https
|
|
12
|
-
* -
|
|
10
|
+
* - Origin 必须精确匹配(scheme + host + port 全等)。不要改成后缀匹配、正则或「包含主站主机名」——
|
|
11
|
+
* `https://<主站主机>.evil.com`、`https://evil.<主站主机>` 都必须被拒。名单作为参数传入:名单为空即全部拒绝。
|
|
12
|
+
* - 默认只放行主站。开发域名只在 `bridge --dev` 时放行:`localhost:5173` 是所有本机 Vite 项目共用的 Origin、
|
|
13
13
|
* 共用 localStorage,给开发版 M2 配对过后,任何跑在 5173 的项目(包括它的依赖)都能读走 token 免码连上
|
|
14
|
-
* (review r3 B2,用户 2026-10-02 拍板改为 --dev
|
|
14
|
+
* (review r3 B2,用户 2026-10-02 拍板改为 --dev,推翻了最初的「开发域名常开」)。名单写死在包里(主站 origin 在
|
|
15
|
+
* `site-config.ts`),不提供任意扩展参数。
|
|
15
16
|
* - Host 校验防 DNS rebinding:攻击者域名解析到 127.0.0.1 时,Host 头是攻击者域名。
|
|
16
17
|
*/
|
|
18
|
+
import { PROD_ORIGINS } from '../site-config.js';
|
|
17
19
|
import { BRIDGE_WS_PATH } from './protocol.js';
|
|
18
|
-
|
|
19
|
-
export const PROD_ORIGINS = ['https://m2ui.ai'];
|
|
20
|
+
export { PROD_ORIGINS };
|
|
20
21
|
/** 本地开发 M2 用(Vite 默认端口);只有 `bridge --dev` 才放行 */
|
|
21
22
|
export const DEV_ORIGINS = ['http://localhost:5173', 'http://127.0.0.1:5173'];
|
|
22
23
|
export function allowedOrigins(dev) {
|
package/dist/bridge/uninstall.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 门槛、停止接受新连接、停 agent、回复与退出在 server.ts / uninstall-call.ts;本文件只管「怎么跑脚本、怎么读结果」。
|
|
4
4
|
*
|
|
5
5
|
* 不在 TypeScript 里再写一份删除逻辑:删除范围只有 `install/uninstall.sh` 一份实现(`connect/local-bridge` 契约 15「卸载脚本」),
|
|
6
|
-
* 终端、agent 应用、M2 Mac
|
|
6
|
+
* 终端、agent 应用、M2 Mac 客户端运行的是主站上它的逐字副本(`connect:install-assets -- --check` 锁住)。
|
|
7
7
|
*
|
|
8
8
|
* 维护约束:
|
|
9
9
|
* - **先删自己的状态文件,再运行脚本。** 脚本只停它能从状态文件找到的 M2 bridge(`--stop` 发 SIGTERM);自己的还在,
|
package/dist/bridge/update.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* 运行它的是**当前版本**的代码;它把新版本下载、校验、冒烟后切成 current,下一步由启动器用新版本拉起。
|
|
4
4
|
*
|
|
5
5
|
* 不变量(`connect/local-bridge` 契约 16 delta):
|
|
6
|
-
* -
|
|
6
|
+
* - 只更新到主站清单指定的版本(清单版本比当前低也照做,这就是回滚),不跟随 npm latest;
|
|
7
7
|
* - 清单只给版本与哈希,下载地址由 release-manifest.ts 固定拼出;任何下载内容与清单哈希不符都换下一个源,全部不符就放弃;
|
|
8
8
|
* - 读清单失败、超时、格式认不出:什么都不改,用当前版本继续;
|
|
9
9
|
* - 新版本先冒烟(`--self-test`),通过才切换;切换用「先建临时软链再改名」,任何时刻 current 都指向一个完整的版本;
|
|
10
10
|
* - 「新版本拉起后没起来」的回退不在这里:新版本可能连模块都加载不了,回退判定在很少改动的启动器里(契约 16 delta);
|
|
11
|
-
* -
|
|
11
|
+
* - 不加清单地址、下载地址的覆盖开关:那会让「只读主站清单」多出一条旁路(design R8)。测试经 UpdateDeps 注入下载。
|
|
12
12
|
*
|
|
13
13
|
* 并发(design N1):不加锁。所有写入先写临时目录 / 临时软链再改名;两个更新同时跑时各自下到自己的临时目录,
|
|
14
14
|
* 后完成的发现版本目录已完整就丢掉自己的,发现 current 已经切好就不再切。
|
package/dist/clients.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// 各客户端的探测 / 写入 / 登录。每家的命令与坑位都经本机逐一实测,见 README。
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { M2_MCP_URL, SERVER_NAME, findInClaudeConfig, findInCodexToml, findInQoderSettings, appendCodexServer, mergeCursorConfig, mergeOpencodeConfig, } from './config-files.js';
|
|
3
|
+
import { M2_MCP_URL, SERVER_NAME, findInClaudeConfig, findInCodexToml, findInQoderSettings, hasCodexNameConflict, hasNameConflictInServers, nameConflictNote, appendCodexServer, mergeCursorConfig, mergeOpencodeConfig, } from './config-files.js';
|
|
4
4
|
import { existsSync, firstExecutable, firstRunnable, home, readJson, readText, run, which, writeJsonWithBackup, writeTextWithBackup, } from './system.js';
|
|
5
5
|
function jsonExisting(path, find) {
|
|
6
6
|
const read = readJson(path);
|
|
7
7
|
return read.kind === 'ok' ? find(read.value) : null;
|
|
8
8
|
}
|
|
9
|
+
/** 用户级 server 表里同名 `m2` 指向别处 → blocked 提示(D-C1);读不出文件按无冲突,由客户端自己的 CLI 报错 */
|
|
10
|
+
function jsonConflict(path, servers) {
|
|
11
|
+
const read = readJson(path);
|
|
12
|
+
return read.kind === 'ok' && hasNameConflictInServers(servers(read.value)) ? nameConflictNote(path) : null;
|
|
13
|
+
}
|
|
14
|
+
function mcpServersOf(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null ? value.mcpServers : undefined;
|
|
16
|
+
}
|
|
9
17
|
function addWith(command, args) {
|
|
10
18
|
const result = run(command, args);
|
|
11
19
|
return result.status === 0 ? { ok: true } : { ok: false, note: result.output };
|
|
@@ -17,7 +25,7 @@ function claudeCode() {
|
|
|
17
25
|
return {
|
|
18
26
|
label: 'Claude Code',
|
|
19
27
|
existing: jsonExisting(join(home, '.claude.json'), findInClaudeConfig),
|
|
20
|
-
blocked:
|
|
28
|
+
blocked: jsonConflict(join(home, '.claude.json'), mcpServersOf),
|
|
21
29
|
actions: [`claude mcp add --transport http --scope user ${SERVER_NAME} ${M2_MCP_URL}`],
|
|
22
30
|
restart: 'Restart Claude Code (claude --continue keeps your conversation).',
|
|
23
31
|
// `claude mcp login` 在非 TTY 下直接失败(实测 "stdin isn't a terminal"),agent 的 shell 就是这种。
|
|
@@ -38,7 +46,7 @@ function codex() {
|
|
|
38
46
|
return {
|
|
39
47
|
label: 'Codex',
|
|
40
48
|
existing: toml ? findInCodexToml(toml) : null,
|
|
41
|
-
blocked: null,
|
|
49
|
+
blocked: toml && !findInCodexToml(toml) && hasCodexNameConflict(toml) ? nameConflictNote(path) : null,
|
|
42
50
|
actions: [`add [mcp_servers.${SERVER_NAME}] to ${path}`],
|
|
43
51
|
// 顺序敏感:login 的本地回调端口挂在 login 进程上,必须授权完成后再重启 Codex。
|
|
44
52
|
restart: 'Fully quit and reopen Codex (after signing in).',
|
|
@@ -58,7 +66,7 @@ function qoder() {
|
|
|
58
66
|
return {
|
|
59
67
|
label: 'Qoder',
|
|
60
68
|
existing: jsonExisting(join(home, '.qoder', 'settings.json'), findInQoderSettings),
|
|
61
|
-
blocked:
|
|
69
|
+
blocked: jsonConflict(join(home, '.qoder', 'settings.json'), mcpServersOf),
|
|
62
70
|
actions: [`qodercli mcp add -s user -t http ${SERVER_NAME} ${M2_MCP_URL}`],
|
|
63
71
|
restart: 'Reopen Qoder if it is running.',
|
|
64
72
|
// qodercli 没有 mcp login 子命令,授权只能在 UI 里点。
|
|
@@ -93,6 +101,8 @@ function opencode() {
|
|
|
93
101
|
if (read.kind === 'invalid')
|
|
94
102
|
return { ...base, blocked: `${path} is not valid JSON (${read.error}).` };
|
|
95
103
|
const merged = mergeOpencodeConfig(read.kind === 'ok' ? read.value : {});
|
|
104
|
+
if (merged.conflict)
|
|
105
|
+
return { ...base, blocked: nameConflictNote(path) };
|
|
96
106
|
return {
|
|
97
107
|
...base,
|
|
98
108
|
existing: merged.existing,
|
|
@@ -123,6 +133,8 @@ function cursor() {
|
|
|
123
133
|
if (read.kind === 'invalid')
|
|
124
134
|
return { ...base, blocked: `${path} is not valid JSON (${read.error}).` };
|
|
125
135
|
const merged = mergeCursorConfig(read.kind === 'ok' ? read.value : {});
|
|
136
|
+
if (merged.conflict)
|
|
137
|
+
return { ...base, blocked: nameConflictNote(path) };
|
|
126
138
|
return {
|
|
127
139
|
...base,
|
|
128
140
|
existing: merged.existing,
|
package/dist/config-files.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// 各客户端配置文件的纯函数读写:查已有 m2 入口、按原格式合并。全部不碰磁盘,便于单测。
|
|
2
|
-
|
|
2
|
+
// MCP 地址来自 site-config.ts(connect 唯一的站点配置来源),这里转出供 cli.ts / clients.ts 沿用原导出名。
|
|
3
|
+
import { M2_MCP_URL, MCP_HOST } from './site-config.js';
|
|
4
|
+
export { M2_MCP_URL };
|
|
3
5
|
export const SERVER_NAME = 'm2';
|
|
4
6
|
function isObject(value) {
|
|
5
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -9,7 +11,7 @@ export function isM2Url(value) {
|
|
|
9
11
|
if (typeof value !== 'string')
|
|
10
12
|
return false;
|
|
11
13
|
try {
|
|
12
|
-
return new URL(value).host ===
|
|
14
|
+
return new URL(value).host === MCP_HOST;
|
|
13
15
|
}
|
|
14
16
|
catch {
|
|
15
17
|
return false;
|
|
@@ -25,6 +27,21 @@ export function findM2Entry(servers) {
|
|
|
25
27
|
}
|
|
26
28
|
return null;
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* server 表里已有名为 `m2`、但不指向 M2 的入口(change domain-cutover design D-C1)。
|
|
32
|
+
* 我们只会以 `m2` 这个名字写入;同名入口指向别处时既不能追加(Codex 的 TOML 会出现重复表、整个文件解析失败),
|
|
33
|
+
* 也不能覆盖(那可能是用户自己的 server,或换域名前留下的旧入口,静默改掉用户看不出发生了什么)。一律报冲突交给用户。
|
|
34
|
+
*/
|
|
35
|
+
export function hasNameConflictInServers(servers) {
|
|
36
|
+
if (!isObject(servers) || !Object.hasOwn(servers, SERVER_NAME))
|
|
37
|
+
return false;
|
|
38
|
+
const entry = servers[SERVER_NAME];
|
|
39
|
+
return !(isObject(entry) && (isM2Url(entry.url) || isM2Url(entry.serverUrl)));
|
|
40
|
+
}
|
|
41
|
+
/** 同名冲突的提示(给 ClientPlan.blocked 用):说明不改的原因与用户要做的事,不打印对方的地址 */
|
|
42
|
+
export function nameConflictNote(where) {
|
|
43
|
+
return `${where} already has an MCP server named "${SERVER_NAME}" that does not point to M2 (for example one left from an old M2 address). Remove or rename it, then run this again.`;
|
|
44
|
+
}
|
|
28
45
|
/** Claude Code `~/.claude.json` 顶层 mcpServers = user scope。 */
|
|
29
46
|
export function findInClaudeConfig(config) {
|
|
30
47
|
return isObject(config) ? findM2Entry(config.mcpServers) : null;
|
|
@@ -56,15 +73,38 @@ export function findInCodexToml(toml) {
|
|
|
56
73
|
}
|
|
57
74
|
return null;
|
|
58
75
|
}
|
|
76
|
+
/** Codex `config.toml` 里已有 `[mcp_servers.m2]` 段且段内 url 不指向 M2(判定见 `hasNameConflictInServers`) */
|
|
77
|
+
export function hasCodexNameConflict(toml) {
|
|
78
|
+
let inTarget = false;
|
|
79
|
+
let found = false;
|
|
80
|
+
let pointsToM2 = false;
|
|
81
|
+
for (const raw of toml.split(/\r?\n/)) {
|
|
82
|
+
const line = raw.trim();
|
|
83
|
+
const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
|
|
84
|
+
if (header || line.startsWith('[')) {
|
|
85
|
+
inTarget = header?.[2] === SERVER_NAME;
|
|
86
|
+
if (inTarget)
|
|
87
|
+
found = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const url = /^url\s*=\s*["']([^"']+)["']/.exec(line);
|
|
91
|
+
if (inTarget && url && isM2Url(url[1]))
|
|
92
|
+
pointsToM2 = true;
|
|
93
|
+
}
|
|
94
|
+
return found && !pointsToM2;
|
|
95
|
+
}
|
|
59
96
|
/** Cursor `~/.cursor/mcp.json`:合并 `{ url }`,保留其余 server。 */
|
|
60
97
|
export function mergeCursorConfig(config) {
|
|
61
98
|
const base = isObject(config) ? config : {};
|
|
62
99
|
const existing = findM2Entry(base.mcpServers);
|
|
63
100
|
if (existing)
|
|
64
|
-
return { existing, config: base };
|
|
101
|
+
return { existing, conflict: false, config: base };
|
|
102
|
+
if (hasNameConflictInServers(base.mcpServers))
|
|
103
|
+
return { existing: null, conflict: true, config: base };
|
|
65
104
|
const servers = isObject(base.mcpServers) ? base.mcpServers : {};
|
|
66
105
|
return {
|
|
67
106
|
existing: null,
|
|
107
|
+
conflict: false,
|
|
68
108
|
config: { ...base, mcpServers: { ...servers, [SERVER_NAME]: { url: M2_MCP_URL } } },
|
|
69
109
|
};
|
|
70
110
|
}
|
|
@@ -81,12 +121,15 @@ export function mergeOpencodeConfig(config) {
|
|
|
81
121
|
const flatEntries = Object.fromEntries(Object.entries(mcp).filter(([key]) => key !== 'servers'));
|
|
82
122
|
const existing = findM2Entry(flatEntries) ?? findM2Entry(nested);
|
|
83
123
|
if (existing)
|
|
84
|
-
return { existing, config: base };
|
|
124
|
+
return { existing, conflict: false, config: base };
|
|
125
|
+
if (hasNameConflictInServers(flatEntries) || hasNameConflictInServers(nested)) {
|
|
126
|
+
return { existing: null, conflict: true, config: base };
|
|
127
|
+
}
|
|
85
128
|
const useNested = nested !== null && Object.keys(flatEntries).length === 0;
|
|
86
129
|
const nextMcp = useNested
|
|
87
130
|
? { ...mcp, servers: { ...nested, [SERVER_NAME]: { type: 'remote', url: M2_MCP_URL } } }
|
|
88
131
|
: { ...mcp, [SERVER_NAME]: { type: 'remote', url: M2_MCP_URL, enabled: true } };
|
|
89
|
-
return { existing: null, config: { ...base, mcp: nextMcp } };
|
|
132
|
+
return { existing: null, conflict: false, config: { ...base, mcp: nextMcp } };
|
|
90
133
|
}
|
|
91
134
|
/**
|
|
92
135
|
* Codex `config.toml` 追加 m2 段。不走 `codex mcp add`:它在没有已存 token 时会顺手启动 OAuth
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connect 唯一的站点配置来源(change domain-config-externalize D1)。
|
|
3
|
+
*
|
|
4
|
+
* connect 随 npm 发布、在用户机器上运行,没有部署期配置可读,所以站点地址以代码常量的形式集中在这里;
|
|
5
|
+
* 包内其余代码只从这里取值。换域名时只改本文件(`package.json` 的 homepage 同批改),随 connect 发版生效。
|
|
6
|
+
*
|
|
7
|
+
* 维护约束:
|
|
8
|
+
* - 这些值是安全边界的一部分:主站 origin 是 bridge 唯一放行的生产 Origin(`connect/local-bridge` 契约 1:名单写死在包里、
|
|
9
|
+
* 精确匹配),主站清单是更新的唯一信任根(契约 16)。不得改成读环境变量、命令行参数或配置文件 —— 那等于给 Origin
|
|
10
|
+
* 白名单与清单地址加了覆盖开关。
|
|
11
|
+
* - 只写下面两个 origin 字面量,派生值一律由它们拼出,不写第二份。两个 origin 保持单行 `export const NAME = '…';`:
|
|
12
|
+
* 不发版的包之间靠读源码比对防漂移,换写法会让这类比对失效。
|
|
13
|
+
* - origin 必须是规范形式(https、不带路径与尾斜杠、不写默认端口):bridge 拿它与浏览器的 Origin 头逐字比较。
|
|
14
|
+
*/
|
|
15
|
+
/** 主站(用户打开画布的网站)的 origin;页面连 bridge 时 Origin 头就是它 */
|
|
16
|
+
export const APP_PUBLIC_ORIGIN = 'https://patch.design';
|
|
17
|
+
/** MCP 服务的 origin;connect 写进各家 agent 客户端配置的 MCP 地址在它下面 */
|
|
18
|
+
export const MCP_ORIGIN = 'https://mcp.m2ui.ai';
|
|
19
|
+
/** 主站主机名(含端口时带端口) */
|
|
20
|
+
export const APP_PUBLIC_HOST = new URL(APP_PUBLIC_ORIGIN).host;
|
|
21
|
+
/** MCP 服务主机名:按它识别已有的 M2 配置项,老入口的 path / query / 尾斜杠不论 */
|
|
22
|
+
export const MCP_HOST = new URL(MCP_ORIGIN).host;
|
|
23
|
+
/** bridge 默认放行的 Origin:只有主站。`www.<主站主机>` 由 m2-router 301 到根域,页面 Origin 不会是它,所以不列 */
|
|
24
|
+
export const PROD_ORIGINS = [APP_PUBLIC_ORIGIN];
|
|
25
|
+
/** 主站清单:bridge 更新时唯一读取的版本与哈希来源 */
|
|
26
|
+
export const MANIFEST_URL = `${APP_PUBLIC_ORIGIN}/connect/manifest.json`;
|
|
27
|
+
/** 写进各家 agent 客户端配置的 MCP 地址(streamable HTTP) */
|
|
28
|
+
export const M2_MCP_URL = `${MCP_ORIGIN}/mcp`;
|
package/install/uninstall.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# M2 local connection uninstaller
|
|
3
|
-
# curl -fsSL https
|
|
2
|
+
# M2 local connection uninstaller, served by the M2 site at /connect/uninstall.sh
|
|
3
|
+
# curl -fsSL https://<M2 site>/connect/uninstall.sh | sh
|
|
4
4
|
#
|
|
5
5
|
# What it does:
|
|
6
6
|
# 1. Stops the M2 bridge if one is running (only a bridge it can recognize, and only with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m2canvas/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Connect your local agent clients (Claude Code, Codex, Cursor, OpenCode, Qoder) to M2 Canvas over MCP in one command, and run the local bridge that lets M2 drive them over ACP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"build": "tsc -p tsconfig.json",
|
|
22
22
|
"prepublishOnly": "npm run build"
|
|
23
23
|
},
|
|
24
|
-
"homepage": "https://
|
|
24
|
+
"homepage": "https://patch.design/mcp",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public",
|