@tianshu-ai/local-bridge 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 +91 -0
- package/dist/browser.js +160 -0
- package/dist/browser.js.map +1 -0
- package/dist/connection.js +128 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# local-bridge
|
|
2
|
+
|
|
3
|
+
`@tianshu-ai/local-bridge` — a lightweight local client that connects
|
|
4
|
+
your machine's tools (browser, and more) into a
|
|
5
|
+
[tianshu](https://github.com/tianshu-ai/tianshu) server via
|
|
6
|
+
**reverse-MCP**.
|
|
7
|
+
|
|
8
|
+
tianshu runs on a server. This bridge runs on **your** computer, dials
|
|
9
|
+
out to the server over a WebSocket, and registers itself as an MCP
|
|
10
|
+
server. The server-side agent can then drive your local tools — as if
|
|
11
|
+
they were built in — but scoped to your own sessions only.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
your machine tianshu server
|
|
15
|
+
┌──────────────┐ outbound WSS ┌──────────────────┐
|
|
16
|
+
│ local-bridge │ ────────────────▶ │ /ws (Local Bridge │
|
|
17
|
+
│ • browser │ register tools │ plugin) │
|
|
18
|
+
│ • echo │ ◀──── tools/call ─│ → agent uses │
|
|
19
|
+
└──────────────┘ ──── result ─────▶ │ your tools │
|
|
20
|
+
└──────────────────┘
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install & run
|
|
24
|
+
|
|
25
|
+
Open the **Local Bridge** panel in tianshu, copy the command it shows,
|
|
26
|
+
and run it on your machine:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @tianshu-ai/local-bridge --server wss://your-tianshu.example.com/ws --token ***
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For a local dev server with auth off, the token is optional:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @tianshu-ai/local-bridge --server ws://localhost:3110/ws
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Flags
|
|
39
|
+
|
|
40
|
+
| flag | meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `--server <url>` | tianshu chat WS endpoint (required), e.g. `wss://host/ws` |
|
|
43
|
+
| `--token <token>` | connection token from the Local Bridge panel |
|
|
44
|
+
| `--device <id>` | stable device id (default: hostname) |
|
|
45
|
+
| `--label <name>` | human label shown in the panel |
|
|
46
|
+
| `--no-browser` | expose only the connectivity `echo` tool |
|
|
47
|
+
| `--headless` | run the browser without a window (default: headful) |
|
|
48
|
+
| `--cdp <url>` | connect to an already-running Chrome's CDP endpoint (default probe `http://127.0.0.1:9222`; `off` to skip) |
|
|
49
|
+
| `--chrome-channel <ch>` | which installed browser to launch: `chrome` (default), `msedge`, … |
|
|
50
|
+
| `--user-data-dir <dir>` | reuse a Chrome profile dir so logins carry over |
|
|
51
|
+
|
|
52
|
+
**No Chromium download.** The bridge uses *your* Chrome:
|
|
53
|
+
|
|
54
|
+
1. If a Chrome is already running with `--remote-debugging-port=9222`,
|
|
55
|
+
it connects to that (your real browser — real cookies + session).
|
|
56
|
+
2. Otherwise it launches your system-installed Google Chrome via
|
|
57
|
+
Playwright's `channel: "chrome"`. Pass `--user-data-dir` to reuse
|
|
58
|
+
your logged-in profile.
|
|
59
|
+
|
|
60
|
+
To expose your everyday browser, start Chrome with a debugging port:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# macOS
|
|
64
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Tools
|
|
68
|
+
|
|
69
|
+
- `echo` — connectivity check.
|
|
70
|
+
- `browser_navigate` / `browser_get_text` / `browser_click` /
|
|
71
|
+
`browser_screenshot` — drive your real local Chrome (via CDP connect
|
|
72
|
+
or system-Chrome launch; automation-detection softened).
|
|
73
|
+
|
|
74
|
+
## Protocol
|
|
75
|
+
|
|
76
|
+
The bridge speaks the reverse-MCP protocol defined authoritatively in the
|
|
77
|
+
server repo: `plugins/reverse-mcp/PROTOCOL.md`. It's a thin envelope
|
|
78
|
+
around standard MCP JSON-RPC over the authenticated chat WebSocket. This
|
|
79
|
+
client imports no tianshu package.
|
|
80
|
+
|
|
81
|
+
## Develop
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install
|
|
85
|
+
npm run build
|
|
86
|
+
node dist/index.js --server ws://localhost:3110/ws --no-browser
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Repo: <https://github.com/tianshu-ai/local-bridge>
|
|
90
|
+
|
|
91
|
+
Apache-2.0.
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Local browser tools, driven by Playwright against the user's OWN
|
|
2
|
+
// Chrome — never a bundled/downloaded Chromium.
|
|
3
|
+
//
|
|
4
|
+
// Acquisition strategy (best → fallback), so we reuse the real browser
|
|
5
|
+
// with its real fingerprint + logged-in cookies and avoid a heavy
|
|
6
|
+
// download:
|
|
7
|
+
//
|
|
8
|
+
// 1. CONNECT over CDP to a Chrome the user already has running with
|
|
9
|
+
// `--remote-debugging-port` (default probe: http://127.0.0.1:9222).
|
|
10
|
+
// This is the user's actual browser — real profile, real session.
|
|
11
|
+
// 2. LAUNCH the system-installed Google Chrome via Playwright's
|
|
12
|
+
// `channel: "chrome"` (no Chromium download). Optionally reuse the
|
|
13
|
+
// user's profile via `userDataDir` so logins carry over. Launched
|
|
14
|
+
// with automation-detection softened.
|
|
15
|
+
//
|
|
16
|
+
// Nothing here downloads a browser; if neither path works the tool
|
|
17
|
+
// returns a clear, actionable error.
|
|
18
|
+
import { textResult } from "./protocol.js";
|
|
19
|
+
// Args that soften automation detection (navigator.webdriver etc.).
|
|
20
|
+
const STEALTH_ARGS = [
|
|
21
|
+
"--disable-blink-features=AutomationControlled",
|
|
22
|
+
"--no-default-browser-check",
|
|
23
|
+
"--no-first-run",
|
|
24
|
+
];
|
|
25
|
+
async function probeCdp(cdpUrl) {
|
|
26
|
+
if (!cdpUrl)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
const base = cdpUrl.replace(/\/$/, "");
|
|
30
|
+
const res = await fetch(`${base}/json/version`, {
|
|
31
|
+
signal: AbortSignal.timeout(2500),
|
|
32
|
+
});
|
|
33
|
+
return res.ok;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Acquire a Page, connecting to the user's Chrome or launching the
|
|
40
|
+
* system Chrome. Cached after first success. */
|
|
41
|
+
async function ensurePage(state, cfg) {
|
|
42
|
+
if (state.page && !state.page.isClosed())
|
|
43
|
+
return state.page;
|
|
44
|
+
const { chromium } = await import("playwright");
|
|
45
|
+
// 1) Connect to an already-running Chrome over CDP.
|
|
46
|
+
if (!state.browser && !state.context && (await probeCdp(cfg.cdpUrl))) {
|
|
47
|
+
try {
|
|
48
|
+
const browser = await chromium.connectOverCDP(cfg.cdpUrl);
|
|
49
|
+
state.browser = browser;
|
|
50
|
+
const ctx = browser.contexts()[0] ?? (await browser.newContext());
|
|
51
|
+
state.context = ctx;
|
|
52
|
+
state.page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
53
|
+
state.how = `connected to your running Chrome at ${cfg.cdpUrl}`;
|
|
54
|
+
cfg.log(state.how);
|
|
55
|
+
return state.page;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
cfg.log(`CDP connect failed (${err instanceof Error ? err.message : String(err)}); falling back to launch`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 2) Launch the system-installed Chrome (no Chromium download).
|
|
62
|
+
if (cfg.userDataDir) {
|
|
63
|
+
// Persistent context reuses the user's profile → logged-in sessions.
|
|
64
|
+
const ctx = await chromium.launchPersistentContext(cfg.userDataDir, {
|
|
65
|
+
channel: cfg.channel,
|
|
66
|
+
headless: !cfg.headful,
|
|
67
|
+
args: STEALTH_ARGS,
|
|
68
|
+
});
|
|
69
|
+
state.context = ctx;
|
|
70
|
+
state.page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
71
|
+
state.how = `launched system ${cfg.channel} with your profile (${cfg.userDataDir})`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const browser = await chromium.launch({
|
|
75
|
+
channel: cfg.channel,
|
|
76
|
+
headless: !cfg.headful,
|
|
77
|
+
args: STEALTH_ARGS,
|
|
78
|
+
});
|
|
79
|
+
state.browser = browser;
|
|
80
|
+
state.context = await browser.newContext();
|
|
81
|
+
state.page = await state.context.newPage();
|
|
82
|
+
state.how = `launched system ${cfg.channel} (ephemeral profile)`;
|
|
83
|
+
}
|
|
84
|
+
cfg.log(state.how);
|
|
85
|
+
return state.page;
|
|
86
|
+
}
|
|
87
|
+
export function makeBrowserTools(cfg) {
|
|
88
|
+
const state = { browser: null, context: null, page: null, how: "" };
|
|
89
|
+
const navigate = {
|
|
90
|
+
descriptor: {
|
|
91
|
+
name: "browser_navigate",
|
|
92
|
+
description: "Open a URL in the user's local browser and return the page title. Uses their real Chrome (real cookies/session).",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: { url: { type: "string", description: "Absolute http(s) URL to open." } },
|
|
96
|
+
required: ["url"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
async run(args) {
|
|
100
|
+
const url = String(args.url ?? "");
|
|
101
|
+
if (!/^https?:\/\//i.test(url))
|
|
102
|
+
return textResult("url must be an absolute http(s) URL", true);
|
|
103
|
+
const page = await ensurePage(state, cfg);
|
|
104
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
|
105
|
+
const title = await page.title();
|
|
106
|
+
return textResult(`Navigated to ${page.url()}\nTitle: ${title}\n(${state.how})`);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const getText = {
|
|
110
|
+
descriptor: {
|
|
111
|
+
name: "browser_get_text",
|
|
112
|
+
description: "Return the visible text of the current page (optionally a CSS selector).",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: { selector: { type: "string", description: "Optional CSS selector; defaults to <body>." } },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
async run(args) {
|
|
119
|
+
if (!state.page || state.page.isClosed())
|
|
120
|
+
return textResult("no page open — call browser_navigate first", true);
|
|
121
|
+
const selector = typeof args.selector === "string" && args.selector ? args.selector : "body";
|
|
122
|
+
const text = await state.page.locator(selector).first().innerText({ timeout: 15_000 }).catch(() => "");
|
|
123
|
+
return textResult(text.slice(0, 8000) || "(no text found)");
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const click = {
|
|
127
|
+
descriptor: {
|
|
128
|
+
name: "browser_click",
|
|
129
|
+
description: "Click the first element matching a CSS selector on the current page.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: { selector: { type: "string" } },
|
|
133
|
+
required: ["selector"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async run(args) {
|
|
137
|
+
if (!state.page || state.page.isClosed())
|
|
138
|
+
return textResult("no page open — call browser_navigate first", true);
|
|
139
|
+
const selector = String(args.selector ?? "");
|
|
140
|
+
await state.page.locator(selector).first().click({ timeout: 15_000 });
|
|
141
|
+
return textResult(`clicked ${selector}\nNow at: ${state.page.url()}`);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const screenshot = {
|
|
145
|
+
descriptor: {
|
|
146
|
+
name: "browser_screenshot",
|
|
147
|
+
description: "Capture a screenshot of the current page (saved on the bridge host).",
|
|
148
|
+
inputSchema: { type: "object", properties: {} },
|
|
149
|
+
},
|
|
150
|
+
async run() {
|
|
151
|
+
if (!state.page || state.page.isClosed())
|
|
152
|
+
return textResult("no page open — call browser_navigate first", true);
|
|
153
|
+
const path = `/tmp/tianshu-local-bridge-shot-${Date.now()}.png`;
|
|
154
|
+
await state.page.screenshot({ path, fullPage: false });
|
|
155
|
+
return textResult(`screenshot saved on the bridge host at ${path}`);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return [navigate, getText, click, screenshot];
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// WebSocket dial-in to a tianshu server + reverse-MCP request handling.
|
|
2
|
+
//
|
|
3
|
+
// The bridge connects OUTBOUND (works behind NAT/firewall), authenticates
|
|
4
|
+
// with a Bearer token, registers its tools, then serves tools/call
|
|
5
|
+
// requests the server sends. Auto-reconnects with backoff.
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
import { MSG, textResult } from "./protocol.js";
|
|
8
|
+
const RECONNECT_BASE_MS = 1000;
|
|
9
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
10
|
+
export class BridgeConnection {
|
|
11
|
+
opts;
|
|
12
|
+
ws = null;
|
|
13
|
+
closed = false;
|
|
14
|
+
attempt = 0;
|
|
15
|
+
toolsByName = new Map();
|
|
16
|
+
log;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
this.log = opts.log ?? ((m) => console.log(m));
|
|
20
|
+
for (const t of opts.tools)
|
|
21
|
+
this.toolsByName.set(t.descriptor.name, t);
|
|
22
|
+
}
|
|
23
|
+
start() {
|
|
24
|
+
this.closed = false;
|
|
25
|
+
this.connect();
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
this.closed = true;
|
|
29
|
+
try {
|
|
30
|
+
this.ws?.send(JSON.stringify({ type: MSG.unregister }));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* ignore */
|
|
34
|
+
}
|
|
35
|
+
this.ws?.close();
|
|
36
|
+
this.ws = null;
|
|
37
|
+
}
|
|
38
|
+
connect() {
|
|
39
|
+
const headers = {};
|
|
40
|
+
if (this.opts.token)
|
|
41
|
+
headers.Authorization = `Bearer ${this.opts.token}`;
|
|
42
|
+
const ws = new WebSocket(this.opts.server, { headers });
|
|
43
|
+
this.ws = ws;
|
|
44
|
+
ws.on("open", () => {
|
|
45
|
+
this.attempt = 0;
|
|
46
|
+
this.log(`connected to ${this.opts.server}`);
|
|
47
|
+
const reg = {
|
|
48
|
+
type: MSG.register,
|
|
49
|
+
deviceId: this.opts.deviceId,
|
|
50
|
+
label: this.opts.label,
|
|
51
|
+
tools: this.opts.tools.map((t) => t.descriptor),
|
|
52
|
+
};
|
|
53
|
+
ws.send(JSON.stringify(reg));
|
|
54
|
+
});
|
|
55
|
+
ws.on("message", (raw) => {
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(raw.toString());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
switch (msg.type) {
|
|
64
|
+
case MSG.registered:
|
|
65
|
+
if (msg.ok) {
|
|
66
|
+
this.log(`registered device "${this.opts.deviceId}" (${this.toolsByName.size} tools)`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.log(`register rejected: ${msg.error ?? "unknown"}`);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case MSG.request:
|
|
73
|
+
void this.handleRequest(msg);
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
// Ignore other chat-channel traffic on the shared /ws.
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
ws.on("close", () => {
|
|
81
|
+
this.ws = null;
|
|
82
|
+
if (this.closed)
|
|
83
|
+
return;
|
|
84
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.attempt, RECONNECT_MAX_MS);
|
|
85
|
+
this.attempt++;
|
|
86
|
+
this.log(`disconnected; reconnecting in ${Math.round(delay / 1000)}s`);
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (!this.closed)
|
|
89
|
+
this.connect();
|
|
90
|
+
}, delay);
|
|
91
|
+
});
|
|
92
|
+
ws.on("error", (err) => {
|
|
93
|
+
this.log(`socket error: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
send(msg) {
|
|
97
|
+
try {
|
|
98
|
+
this.ws?.send(JSON.stringify(msg));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.log(`failed to send response: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async handleRequest(req) {
|
|
105
|
+
if (req.method !== "tools/call") {
|
|
106
|
+
this.send({ type: MSG.response, id: req.id, error: { code: -32601, message: `unsupported method: ${req.method}` } });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const params = (req.params ?? {});
|
|
110
|
+
const tool = params.name ? this.toolsByName.get(params.name) : undefined;
|
|
111
|
+
if (!tool) {
|
|
112
|
+
this.send({ type: MSG.response, id: req.id, error: { code: -32602, message: `unknown tool: ${params.name}` } });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const result = await tool.run(params.arguments ?? {});
|
|
117
|
+
this.send({ type: MSG.response, id: req.id, result });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
this.send({
|
|
121
|
+
type: MSG.response,
|
|
122
|
+
id: req.id,
|
|
123
|
+
result: textResult(`tool "${params.name}" failed: ${err instanceof Error ? err.message : String(err)}`, true),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,0EAA0E;AAC1E,mEAAmE;AACnE,2DAA2D;AAE3D,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,GAAG,EAAE,UAAU,EAAuE,MAAM,eAAe,CAAC;AAWrH,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,MAAM,OAAO,gBAAgB;IAOE;IANrB,EAAE,GAAqB,IAAI,CAAC;IAC5B,MAAM,GAAG,KAAK,CAAC;IACf,OAAO,GAAG,CAAC,CAAC;IACH,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC3C,GAAG,CAAsB;IAE1C,YAA6B,IAAmB;QAAnB,SAAI,GAAJ,IAAI,CAAe;QAC9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;QACD,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;IACjB,CAAC;IAEO,OAAO;QACb,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,aAAa,GAAG,UAAU,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACzE,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACxD,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QAEb,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,gBAAgB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7C,MAAM,GAAG,GAAgB;gBACvB,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAC5B,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK;gBACtB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;aAChD,CAAC;YACF,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,IAAI,GAAgD,CAAC;YACrD,IAAI,CAAC;gBACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO;YACT,CAAC;YACD,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;gBACjB,KAAK,GAAG,CAAC,UAAU;oBACjB,IAAK,GAAwB,CAAC,EAAE,EAAE,CAAC;wBACjC,IAAI,CAAC,GAAG,CAAC,sBAAsB,IAAI,CAAC,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC,CAAC;oBACzF,CAAC;yBAAM,CAAC;wBACN,IAAI,CAAC,GAAG,CAAC,sBAAuB,GAA0B,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC;oBACnF,CAAC;oBACD,MAAM;gBACR,KAAK,GAAG,CAAC,OAAO;oBACd,KAAK,IAAI,CAAC,aAAa,CAAC,GAA4B,CAAC,CAAC;oBACtD,MAAM;gBACR;oBACE,uDAAuD;oBACvD,MAAM;YACV,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAClB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;YACf,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO;YACxB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;YAChF,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,iCAAiC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACvE,UAAU,CAAC,GAAG,EAAE;gBACd,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,CAAC,EAAE,KAAK,CAAC,CAAC;QACZ,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACrB,IAAI,CAAC,GAAG,CAAC,iBAAiB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,IAAI,CAAC,GAAgB;QAC3B,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3F,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAe;QACzC,IAAI,GAAG,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;YACrH,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAA2D,CAAC;QAC5F,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,iBAAiB,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YAChH,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;YACtD,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,GAAG,CAAC,QAAQ;gBAClB,EAAE,EAAE,GAAG,CAAC,EAAE;gBACV,MAAM,EAAE,UAAU,CAAC,SAAS,MAAM,CAAC,IAAI,aAAa,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC;aAC9G,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @tianshu-ai/local-bridge — dial into a tianshu server and expose local
|
|
3
|
+
// tools (browser, …) to the agent via reverse-MCP.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// npx @tianshu-ai/local-bridge --server wss://tianshu.example.com/ws --token ***
|
|
7
|
+
// npx @tianshu-ai/local-bridge --server ws://localhost:3110/ws (dev, no auth)
|
|
8
|
+
//
|
|
9
|
+
// Flags:
|
|
10
|
+
// --server <url> tianshu chat WS endpoint (required)
|
|
11
|
+
// --token <token> connection token from the Local Bridge panel
|
|
12
|
+
// --device <id> stable device id (default: hostname)
|
|
13
|
+
// --label <name> human label shown in the panel (default: device id)
|
|
14
|
+
// --no-browser don't expose browser tools (echo only)
|
|
15
|
+
// --headless run the browser without a window (default: headful)
|
|
16
|
+
// --cdp <url> connect to an already-running Chrome's CDP endpoint
|
|
17
|
+
// (default probe: http://127.0.0.1:9222). Set to "off"
|
|
18
|
+
// to skip and always launch.
|
|
19
|
+
// --chrome-channel which installed browser to launch: chrome (default),
|
|
20
|
+
// msedge, chrome-beta, … (never downloads Chromium)
|
|
21
|
+
// --user-data-dir reuse a Chrome profile dir so logins carry over
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import { BridgeConnection } from "./connection.js";
|
|
24
|
+
import { makeBrowserTools } from "./browser.js";
|
|
25
|
+
import { textResult } from "./protocol.js";
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
if (!a.startsWith("--"))
|
|
31
|
+
continue;
|
|
32
|
+
const key = a.slice(2);
|
|
33
|
+
const next = argv[i + 1];
|
|
34
|
+
if (next && !next.startsWith("--")) {
|
|
35
|
+
out[key] = next;
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
out[key] = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function main() {
|
|
45
|
+
const args = parseArgs(process.argv.slice(2));
|
|
46
|
+
const server = typeof args.server === "string" ? args.server : "";
|
|
47
|
+
if (!server) {
|
|
48
|
+
console.error("error: --server <wss://host/ws> is required");
|
|
49
|
+
console.error("example: npx @tianshu-ai/local-bridge --server wss://tianshu.example.com/ws --token ***");
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
const token = typeof args.token === "string" ? args.token : undefined;
|
|
53
|
+
const deviceId = (typeof args.device === "string" && args.device) || os.hostname() || "bridge";
|
|
54
|
+
const label = typeof args.label === "string" ? args.label : deviceId;
|
|
55
|
+
// Default headful: a real browser window the user can watch. --headless to hide.
|
|
56
|
+
const headful = args.headless !== true;
|
|
57
|
+
const cdpArg = typeof args.cdp === "string" ? args.cdp : "http://127.0.0.1:9222";
|
|
58
|
+
const cdpUrl = cdpArg === "off" ? "" : cdpArg;
|
|
59
|
+
const channel = typeof args["chrome-channel"] === "string" ? args["chrome-channel"] : "chrome";
|
|
60
|
+
const userDataDir = typeof args["user-data-dir"] === "string" ? args["user-data-dir"] : "";
|
|
61
|
+
// Always ship a trivial echo tool so the round-trip can be verified
|
|
62
|
+
// without a browser install.
|
|
63
|
+
const echo = {
|
|
64
|
+
descriptor: {
|
|
65
|
+
name: "echo",
|
|
66
|
+
description: "Echo the given text back (bridge connectivity check).",
|
|
67
|
+
inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
|
|
68
|
+
},
|
|
69
|
+
async run(a) {
|
|
70
|
+
return textResult(String(a.text ?? ""));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const browserOn = args["no-browser"] !== true;
|
|
74
|
+
const tools = [echo];
|
|
75
|
+
if (browserOn) {
|
|
76
|
+
tools.push(...makeBrowserTools({
|
|
77
|
+
cdpUrl,
|
|
78
|
+
channel,
|
|
79
|
+
userDataDir,
|
|
80
|
+
headful,
|
|
81
|
+
log: (m) => console.log(`[local-bridge] ${m}`),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
const conn = new BridgeConnection({
|
|
85
|
+
server,
|
|
86
|
+
token,
|
|
87
|
+
deviceId,
|
|
88
|
+
label,
|
|
89
|
+
tools,
|
|
90
|
+
log: (m) => console.log(`[local-bridge] ${m}`),
|
|
91
|
+
});
|
|
92
|
+
console.log(`[local-bridge] starting — device="${deviceId}", ${tools.length} tools, ` +
|
|
93
|
+
`browser=${browserOn ? (cdpUrl ? `connect(${cdpUrl}) or ${channel}` : channel) : "off"}`);
|
|
94
|
+
conn.start();
|
|
95
|
+
const shutdown = () => {
|
|
96
|
+
console.log("\n[local-bridge] shutting down…");
|
|
97
|
+
conn.stop();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
};
|
|
100
|
+
process.on("SIGINT", shutdown);
|
|
101
|
+
process.on("SIGTERM", shutdown);
|
|
102
|
+
}
|
|
103
|
+
main();
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Reverse-MCP wire protocol (client side). Mirrors the authoritative
|
|
2
|
+
// contract in the tianshu server's plugins/reverse-mcp/PROTOCOL.md. We
|
|
3
|
+
// deliberately do NOT import any tianshu package — the bridge stays a
|
|
4
|
+
// standalone client that speaks the protocol.
|
|
5
|
+
export const MSG = {
|
|
6
|
+
register: "reverse_mcp_register",
|
|
7
|
+
unregister: "reverse_mcp_unregister",
|
|
8
|
+
response: "reverse_mcp_response",
|
|
9
|
+
request: "reverse_mcp_request",
|
|
10
|
+
registered: "reverse_mcp_registered",
|
|
11
|
+
};
|
|
12
|
+
export function textResult(text, isError = false) {
|
|
13
|
+
return { content: [{ type: "text", text }], isError };
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,uEAAuE;AACvE,sEAAsE;AACtE,8CAA8C;AAE9C,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,QAAQ,EAAE,sBAAsB;IAChC,UAAU,EAAE,wBAAwB;IACpC,QAAQ,EAAE,sBAAsB;IAChC,OAAO,EAAE,qBAAqB;IAC9B,UAAU,EAAE,wBAAwB;CAC5B,CAAC;AAqBX,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK;IACtD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;AACxD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tianshu-ai/local-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local bridge for tianshu — dial into a tianshu server over WebSocket and expose your machine's tools (browser, files, shell) to the agent via reverse-MCP.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tianshu-local-bridge": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"playwright": "^1.56.0",
|
|
23
|
+
"ws": "^8.18.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.0.0",
|
|
27
|
+
"@types/ws": "^8.5.12",
|
|
28
|
+
"typescript": "^5.6.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|