Skip to content

Commit 6806178

Browse files
committed
v1.1.2
Security: - Add Bearer Token auth for Streamable HTTP mode (--host, --token flags) - Require token when binding to non-loopback address Bug Fixes: - Fix @mention false positives by excluding /@ pattern (macOS & Windows) - Improve macOS activateLine stability with retry mechanism (up to 3 retries) - Improve macOS selectChat reliability with window existence check and error handling -- 安全性強化: - Streamable HTTP 模式新增 Bearer Token 驗證(--host、--token 參數) - 綁定非 loopback 位址時強制要求提供 Token Bug 修復: - 修正 @mention 解析誤判,排除 /@ 格式(macOS & Windows) - 修正 macOS activateLine 穩定性,新增重試機制(最多 3 次) - 修正 macOS selectChat 穩定性,新增 window 存在性檢查與錯誤處理
1 parent 4023b0c commit 6806178

4 files changed

Lines changed: 153 additions & 12 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,18 @@ LINE Desktop MCP 是一個基於 Model Context Protocol 的整合工具,讓 AI
123123
**啟動 Streamable HTTP 模式:**
124124

125125
```bash
126+
# 本機使用(僅 localhost)
126127
npx line-desktop-mcp@latest --http-mode --port 3000
128+
129+
# 開放外部連線(需搭配 token)
130+
npx line-desktop-mcp@latest --http-mode --host 0.0.0.0 --port 3000 --token YOUR_SECRET
127131
```
128132

129133
**參數說明:**
130134
- `--http-mode`:啟用 Streamable HTTP 模式,使用 HTTP streaming 而非 stdio
131135
- `--port <port>`:指定 HTTP 伺服器的 port(預設:3000)
136+
- `--host <host>`:指定綁定的網路介面(預設:`127.0.0.1`
137+
- `--token <secret>`:設定 Bearer Token 驗證密鑰。當 `--host` 設為非 loopback 位址時為必填,以確保安全性
132138

133139
**MCP 端點配置:**
134140

@@ -323,12 +329,18 @@ In addition to the default stdio mode, this project also supports running via St
323329
**Start Streamable HTTP Mode:**
324330

325331
```bash
332+
# Local use (localhost only)
326333
npx line-desktop-mcp@latest --http-mode --port 3000
334+
335+
# Allow external connections (token required)
336+
npx line-desktop-mcp@latest --http-mode --host 0.0.0.0 --port 3000 --token YOUR_SECRET
327337
```
328338

329339
**Parameters:**
330340
- `--http-mode`: Enable Streamable HTTP mode, using HTTP streaming instead of stdio
331341
- `--port <port>`: Specify the HTTP server port (default: 3000)
342+
- `--host <host>`: Specify the network interface to bind (default: `127.0.0.1`)
343+
- `--token <secret>`: Set Bearer Token authentication secret. Required when `--host` is set to a non-loopback address for security
332344

333345
**MCP Endpoint Configuration:**
334346

src/automation/macos-line-automation.js

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ export class MacOSLineAutomation {
9393
tell application "${this.appleEsc(this.lineAppName)}"
9494
activate
9595
end tell
96+
97+
-- 檢查是否成功成為前景
98+
tell application "System Events"
99+
repeat with i from 1 to 3
100+
if frontmost of process "${this.appleEsc(this.lineAppName)}" is true then
101+
exit repeat
102+
else
103+
delay 0.5
104+
tell application "${this.appleEsc(this.lineAppName)}" to activate
105+
end if
106+
end repeat
107+
end tell
108+
109+
-- 檢查是否超過重試次數
110+
tell application "System Events"
111+
if frontmost of process "${this.appleEsc(this.lineAppName)}" is false then
112+
error "無法將 LINE 置於前景。"
113+
end if
114+
end tell
115+
96116
`;
97117
try {
98118
await execAppleScript(script);
@@ -111,10 +131,39 @@ export class MacOSLineAutomation {
111131
112132
set the clipboard to "${this.appleEsc(chatName)}"
113133
114-
-- 叫出搜尋用點擊
115-
-- 依你原始路徑抓元素clea
116-
set theSearch to text field 1 of splitter group 1 of window 1
117-
134+
-- 點擊『聊天』Icon
135+
try
136+
set the lineWin to window 1
137+
on error errMsg number errNum
138+
if errNum is -1719 then
139+
return false
140+
else
141+
error errMsg number errNum
142+
end if
143+
end try
144+
145+
-- 取得位置與大小(全域座標)
146+
set {xPosition, yPosition} to position of lineWin
147+
set {xSize, ySize} to size of lineWin
148+
149+
set cx to xPosition + 32
150+
set cy to yPosition + 110
151+
152+
${this.cliclickShellClick('cx', 'cy')}
153+
154+
delay ${this.delayMid}
155+
156+
-- 點擊『搜尋』textbox
157+
try
158+
set theSearch to text field 1 of splitter group 1 of window 1
159+
on error errMsg number errNum
160+
if errNum is -1719 then
161+
return false
162+
else
163+
error errMsg number errNum
164+
end if
165+
end try
166+
118167
-- 取得位置與大小(全域座標)
119168
set {xPosition, yPosition} to position of theSearch
120169
set {xSize, ySize} to size of theSearch
@@ -267,6 +316,7 @@ export class MacOSLineAutomation {
267316
let currentPart = '';
268317

269318
// Use regex to split by @mentions pattern
319+
/*
270320
const parts = message.split(/(@\S+\s)/g);
271321
272322
for (let i = 0; i < parts.length; i++) {
@@ -284,7 +334,27 @@ export class MacOSLineAutomation {
284334
currentPart += part;
285335
}
286336
}
337+
*/
338+
339+
// Use regex to split by @mentions pattern (已排除 /@ 這個格式)
340+
const parts = message.split(/((?<!\/)@\S+\s)/g);
287341

342+
for (let i = 0; i < parts.length; i++) {
343+
const part = parts[i];
344+
if (part.match(/^(?<!\/)@\S+\s$/)) {
345+
// If we have accumulated text before this @mention, add it as a separate part
346+
if (currentPart) {
347+
messageParts.push(currentPart);
348+
currentPart = '';
349+
}
350+
// Add the @mention as its own part
351+
messageParts.push(part);
352+
} else {
353+
// Accumulate non-@mention text
354+
currentPart += part;
355+
}
356+
}
357+
288358
// Add any remaining text
289359
if (currentPart) {
290360
messageParts.push(currentPart);

src/automation/windows-line-automation.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ ${script}
236236
const messageParts = [];
237237
let currentPart = '';
238238

239+
/*
239240
const parts = message.split(/(@\S+\s)/g);
240241
241242
for (let i = 0; i < parts.length; i++) {
@@ -250,7 +251,28 @@ ${script}
250251
currentPart += part;
251252
}
252253
}
254+
*/
255+
256+
// Use regex to split by @mentions pattern (已排除 /@ 這個格式)
257+
const parts = message.split(/((?<!\/)@\S+\s)/g);
258+
259+
for (let i = 0; i < parts.length; i++) {
260+
const part = parts[i];
261+
if (part.match(/^(?<!\/)@\S+\s$/)) {
262+
// If we have accumulated text before this @mention, add it as a separate part
263+
if (currentPart) {
264+
messageParts.push(currentPart);
265+
currentPart = '';
266+
}
267+
// Add the @mention as its own part
268+
messageParts.push(part);
269+
} else {
270+
// Accumulate non-@mention text
271+
currentPart += part;
272+
}
273+
}
253274

275+
// Add any remaining text
254276
if (currentPart) {
255277
messageParts.push(currentPart);
256278
}

src/server.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ class LineDesktopMCPServer {
435435
}
436436

437437

438-
async run(useSSE = false, port = 3000) {
438+
async run(useSSE = false, port = 3000, host = '127.0.0.1', token = null) {
439439
if (useSSE) {
440440
// 使用 Streamable HTTP 模式(符合 MCP 2025-06-18 規範)
441441
const app = express();
@@ -454,6 +454,23 @@ class LineDesktopMCPServer {
454454

455455
app.use(express.json());
456456

457+
// Bearer token 驗證中間件
458+
if (token) {
459+
app.use('/mcp', (req, res, next) => {
460+
const authHeader = req.headers['authorization'];
461+
if (!authHeader || authHeader !== `Bearer ${token}`) {
462+
console.error(`[AUTH] Rejected request from ${req.ip} - invalid or missing token`);
463+
res.status(401).json({
464+
jsonrpc: '2.0',
465+
error: { code: -32001, message: 'Unauthorized: invalid or missing Bearer token' },
466+
id: null
467+
});
468+
return;
469+
}
470+
next();
471+
});
472+
}
473+
457474
// MCP 端點路徑
458475
const endpoint = '/mcp';
459476

@@ -555,11 +572,13 @@ class LineDesktopMCPServer {
555572
});
556573

557574

558-
app.listen(port, '0.0.0.0', () => {
575+
app.listen(port, host, () => {
559576
console.error(`LINE Desktop MCP Server running on Streamable HTTP mode`);
560-
console.error(` Local: http://127.0.0.1:${port}${endpoint}`);
561-
console.error(` Network: http://0.0.0.0:${port}${endpoint}`);
562-
console.error(` Health: http://127.0.0.1:${port}/health`);
577+
console.error(` Listening: http://${host}:${port}${endpoint}`);
578+
console.error(` Health: http://${host}:${port}/health`);
579+
if (token) {
580+
console.error(` Auth: Bearer token required`);
581+
}
563582
console.error(`Ready to accept connections...`);
564583
});
565584
} else {
@@ -576,7 +595,9 @@ function parseArgs() {
576595
const args = process.argv.slice(2);
577596
const config = {
578597
sseMode: false,
579-
port: 3000
598+
port: 3000,
599+
host: '127.0.0.1',
600+
token: null
580601
};
581602

582603
for (let i = 0; i < args.length; i++) {
@@ -585,6 +606,12 @@ function parseArgs() {
585606
} else if (args[i] === '--port' && i + 1 < args.length) {
586607
config.port = parseInt(args[i + 1], 10);
587608
i++; // 跳過下一個參數
609+
} else if (args[i] === '--host' && i + 1 < args.length) {
610+
config.host = args[i + 1];
611+
i++;
612+
} else if (args[i] === '--token' && i + 1 < args.length) {
613+
config.token = args[i + 1];
614+
i++;
588615
}
589616
}
590617

@@ -596,11 +623,21 @@ await firstRunSetup();
596623

597624
// 解析命令列參數並啟動伺服器
598625
const config = parseArgs();
626+
627+
// 安全檢查:非 loopback 綁定必須提供 token
628+
if (config.sseMode && config.host !== '127.0.0.1' && config.host !== 'localhost') {
629+
if (!config.token) {
630+
console.error('ERROR: Binding to a non-loopback address requires --token <secret> for authentication.');
631+
console.error(' Example: npx line-desktop-mcp --http-mode --host 0.0.0.0 --port 3000 --token MY_SECRET');
632+
process.exit(1);
633+
}
634+
}
635+
599636
const server = new LineDesktopMCPServer();
600637

601638
if (config.sseMode) {
602-
console.error(`Starting server in Streamable HTTP mode on port ${config.port}`);
603-
server.run(true, config.port).catch(console.error);
639+
console.error(`Starting server in Streamable HTTP mode on ${config.host}:${config.port}`);
640+
server.run(true, config.port, config.host, config.token).catch(console.error);
604641
} else {
605642
console.error('Starting server in stdio mode');
606643
server.run(false).catch(console.error);

0 commit comments

Comments
 (0)