From 61d598142df6b52b593eaf781db95855382376ff Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:14:44 +0800 Subject: [PATCH] feat(ext): ask user approval for MCP task --- package-lock.json | 1 - packages/extension/package.json | 1 - .../extension/src/components/ConfigPanel.tsx | 18 +- .../extension/src/components/ui/switch.tsx | 2 +- .../extension/src/entrypoints/hub/App.tsx | 205 +++++++++++++----- .../extension/src/entrypoints/hub/hub-ws.ts | 26 ++- 6 files changed, 191 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index f05ccfa..b9c2647 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11832,7 +11832,6 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tooltip": "^1.2.8", "@types/chrome": "^0.1.37", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.1", diff --git a/packages/extension/package.json b/packages/extension/package.json index b5fbbb1..ea4e391 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -16,7 +16,6 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tooltip": "^1.2.8", "@types/chrome": "^0.1.37", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.1", diff --git a/packages/extension/src/components/ConfigPanel.tsx b/packages/extension/src/components/ConfigPanel.tsx index e063c7e..5443ba7 100644 --- a/packages/extension/src/components/ConfigPanel.tsx +++ b/packages/extension/src/components/ConfigPanel.tsx @@ -1,13 +1,14 @@ import { - ChevronDown, Copy, CornerUpLeft, Eye, EyeOff, + FoldVertical, HatGlasses, Home, Loader2, Scale, + UnfoldVertical, } from 'lucide-react' import { useEffect, useState } from 'react' import { siGithub } from 'simple-icons' @@ -153,6 +154,16 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) { + {/* Hub link */} + + Manage Page Agent Hub config + + +
Advanced - + {advancedOpen ? : } {advancedOpen && ( diff --git a/packages/extension/src/components/ui/switch.tsx b/packages/extension/src/components/ui/switch.tsx index 8c29424..31fa2cb 100644 --- a/packages/extension/src/components/ui/switch.tsx +++ b/packages/extension/src/components/ui/switch.tsx @@ -8,7 +8,7 @@ function Switch({ className, ...props }: React.ComponentProps -
+ Page Agent Hub + + Beta + + + +
+
+

+ Page Agent Hub lets local apps (e.g. MCP servers) control the Page Agent extension via + WebSocket. +

+

+ Check out the official{' '} + + MCP server package + + . +

+
+ + + +
-
- -
- -
- Connect via hub.html?ws=PORT +
+ v{__VERSION__} + + Built with ♥️ by{' '} + + @Simon + +
{/* Right — Live session */}
- {/* Header bar */} -
+
{wsLabel} @@ -104,49 +144,108 @@ export default function App() { ) } -function ProtocolDocs() { +function HubConfig() { + const [allowAll, setAllowAll] = useState(false) + + useEffect(() => { + chrome.storage.local.get('allowAllHubConnection').then((r) => { + setAllowAll(r.allowAllHubConnection === true) + }) + }, []) + + const toggle = (checked: boolean) => { + setAllowAll(checked) + chrome.storage.local.set({ allowAllHubConnection: checked }) + } + return ( -
-
-

- Caller → Hub -

-
-					{`{ type: "execute", task: string, config?: object }
-{ type: "stop" }`}
-				
-
+
+

+ Config +

+
+ -
-

- Hub → Caller -

-
-					{`{ type: "ready" }
-{ type: "result", success: boolean, data: string }
-{ type: "error", message: string }`}
-				
-
- -
-

- Flow -

-
    -
  1. Hub opens WS to caller's server
  2. -
  3. - Sends ready -
  4. -
  5. - Caller sends execute with task -
  6. -
  7. Hub runs agent, streams events
  8. -
  9. - Hub sends result or{' '} - error -
  10. -
-
+ {/* hide with invisible absolute opacity-0*/} +
+
+
+ By default, each connection requires your approval before running tasks.
+ Enable this to skip per-session approval. +
+ * Use with caution! +
+
+
+
+ ) +} + +function ProtocolDocsCollapsible() { + const [open, setOpen] = useState(false) + + return ( +
+ + + {open && ( +
+

+ Connect via hub.html?ws=PORT +

+ +
+

Flow

+
    +
  1. Hub opens WS to caller's server
  2. +
  3. + Sends ready +
  4. +
  5. + Caller sends execute with task +
  6. +
  7. Hub runs agent, streams events
  8. +
  9. + Hub sends result or{' '} + error +
  10. +
+
+ +
+

Caller → Hub

+
+							{`{ type: "execute", task: string, config?: object }
+{ type: "stop" }`}
+						
+
+ +
+

Hub → Caller

+
+							{`{ type: "ready" }
+{ type: "result", success: boolean, data: string }
+{ type: "error", message: string }`}
+						
+
+
+ )}
) } diff --git a/packages/extension/src/entrypoints/hub/hub-ws.ts b/packages/extension/src/entrypoints/hub/hub-ws.ts index e76f6d4..ed341b9 100644 --- a/packages/extension/src/entrypoints/hub/hub-ws.ts +++ b/packages/extension/src/entrypoints/hub/hub-ws.ts @@ -70,6 +70,7 @@ export class HubWs { #ws: WebSocket | null = null #state: HubWsState = 'disconnected' #busy = false + #approved = false #handlers: HubWsHandlers #port: number #onStateChange: (state: HubWsState) => void @@ -103,6 +104,7 @@ export class HubWs { ws.addEventListener('close', () => { this.#ws = null this.#busy = false + this.#approved = false this.#setState('disconnected') }) @@ -115,6 +117,7 @@ export class HubWs { this.#ws?.close() this.#ws = null this.#busy = false + this.#approved = false this.#setState('disconnected') } @@ -130,7 +133,7 @@ export class HubWs { } } - #handleMessage(raw: string) { + async #handleMessage(raw: string) { let msg: InboundMessage try { msg = JSON.parse(raw) @@ -138,6 +141,11 @@ export class HubWs { return } + if (!(await this.#checkApproval())) { + this.#send({ type: 'error', message: 'User denied the connection request.' }) + return + } + switch (msg.type) { case 'execute': this.#handleExecute(msg) @@ -148,6 +156,22 @@ export class HubWs { } } + async #checkApproval(): Promise { + if (this.#approved) return true + + const { allowAllHubConnection } = await chrome.storage.local.get('allowAllHubConnection') + if (allowAllHubConnection === true) { + this.#approved = true + return true + } + + const ok = window.confirm( + 'An external application is requesting to control your browser via Page Agent Ext.\nAllow this session?' + ) + if (ok) this.#approved = true + return ok + } + async #handleExecute(msg: ExecuteMessage) { if (this.#busy) { this.#send({ type: 'error', message: 'Hub is busy with another task' })