This is page 1 of 8. Use http://codebase.md/hangwin/mcp-chrome?page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
*.onnx filter=lfs diff=lfs merge=lfs -text
```
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
```json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"printWidth": 100,
"endOfLine": "auto",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "strict"
}
```
--------------------------------------------------------------------------------
/packages/wasm-simd/.gitignore:
--------------------------------------------------------------------------------
```
# WASM build outputs
/pkg/
/target/
# Rust
Cargo.lock
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
```
--------------------------------------------------------------------------------
/app/chrome-extension/.env.example:
--------------------------------------------------------------------------------
```
# Chrome Extension Private Key
# Copy this file to .env and replace with your actual private key
# This key is used for Chrome extension packaging and should be kept secure
CHROME_EXTENSION_KEY=YOUR_PRIVATE_KEY_HERE
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts
dist
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.onnx
# Environment variables
.env
.env.local
.env.*.local
# Prevent npm metadata pollution
false/
metadata-v1.3/
registry.npmmirror.com/
registry.npmjs.com/
```
--------------------------------------------------------------------------------
/app/chrome-extension/README.md:
--------------------------------------------------------------------------------
```markdown
# WXT + Vue 3
This template should help get you started developing with Vue 3 in WXT.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
```
--------------------------------------------------------------------------------
/packages/wasm-simd/README.md:
--------------------------------------------------------------------------------
```markdown
# @chrome-mcp/wasm-simd
SIMD-optimized WebAssembly math functions for high-performance vector operations.
## Features
- 🚀 **SIMD Acceleration**: Uses WebAssembly SIMD instructions for 4-8x performance boost
- 🧮 **Vector Operations**: Optimized cosine similarity, batch processing, and matrix operations
- 🔧 **Memory Efficient**: Smart memory pooling and aligned buffer management
- 🌐 **Browser Compatible**: Works in all modern browsers with WebAssembly SIMD support
## Performance
| Operation | JavaScript | SIMD WASM | Speedup |
| ------------------------------ | ---------- | --------- | ------- |
| Cosine Similarity (768d) | 100ms | 18ms | 5.6x |
| Batch Similarity (100x768d) | 850ms | 95ms | 8.9x |
| Similarity Matrix (50x50x384d) | 2.1s | 180ms | 11.7x |
## Usage
```rust
// The Rust implementation provides SIMD-optimized functions
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct SIMDMath;
#[wasm_bindgen]
impl SIMDMath {
#[wasm_bindgen(constructor)]
pub fn new() -> SIMDMath { SIMDMath }
#[wasm_bindgen]
pub fn cosine_similarity(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
// SIMD-optimized implementation
}
}
```
## Building
```bash
# Install dependencies
cargo install wasm-pack
# Build for release
npm run build
# Build for development
npm run build:dev
```
## Browser Support
- Chrome 91+
- Firefox 89+
- Safari 16.4+
- Edge 91+
Older browsers automatically fall back to JavaScript implementations.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Chrome MCP Server 🚀
[](https://img.shields.io/github/stars/hangwin/mcp-chrome)
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
[](https://developer.chrome.com/docs/extensions/)
[](https://img.shields.io/github/v/release/hangwin/mcp-chrome.svg)
> 🌟 **Turn your Chrome browser into your intelligent assistant** - Let AI take control of your browser, transforming it into a powerful AI-controlled automation tool.
**📖 Documentation**: [English](README.md) | [中文](README_zh.md)
> The project is still in its early stages and is under intensive development. More features, stability improvements, and other enhancements will follow.
---
## 🎯 What is Chrome MCP Server?
Chrome MCP Server is a Chrome extension-based **Model Context Protocol (MCP) server** that exposes your Chrome browser functionality to AI assistants like Claude, enabling complex browser automation, content analysis, and semantic search. Unlike traditional browser automation tools (like Playwright), **Chrome MCP Server** directly uses your daily Chrome browser, leveraging existing user habits, configurations, and login states, allowing various large models or chatbots to take control of your browser and truly become your everyday assistant.
## ✨ Core Features
- 😁 **Chatbot/Model Agnostic**: Let any LLM or chatbot client or agent you prefer automate your browser
- ⭐️ **Use Your Original Browser**: Seamlessly integrate with your existing browser environment (your configurations, login states, etc.)
- 💻 **Fully Local**: Pure local MCP server ensuring user privacy
- 🚄 **Streamable HTTP**: Streamable HTTP connection method
- 🏎 **Cross-Tab**: Cross-tab context
- 🧠 **Semantic Search**: Built-in vector database for intelligent browser tab content discovery
- 🔍 **Smart Content Analysis**: AI-powered text extraction and similarity matching
- 🌐 **20+ Tools**: Support for screenshots, network monitoring, interactive operations, bookmark management, browsing history, and 20+ other tools
- 🚀 **SIMD-Accelerated AI**: Custom WebAssembly SIMD optimization for 4-8x faster vector operations
## 🆚 Comparison with Similar Projects
| Comparison Dimension | Playwright-based MCP Server | Chrome Extension-based MCP Server |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Resource Usage** | ❌ Requires launching independent browser process, installing Playwright dependencies, downloading browser binaries, etc. | ✅ No need to launch independent browser process, directly utilizes user's already open Chrome browser |
| **User Session Reuse** | ❌ Requires re-login | ✅ Automatically uses existing login state |
| **Browser Environment** | ❌ Clean environment lacks user settings | ✅ Fully preserves user environment |
| **API Access** | ⚠️ Limited to Playwright API | ✅ Full access to Chrome native APIs |
| **Startup Speed** | ❌ Requires launching browser process | ✅ Only needs to activate extension |
| **Response Speed** | 50-200ms inter-process communication | ✅ Faster |
## 🚀 Quick Start
### Prerequisites
- Node.js >= 18.19.0 and pnpm/npm
- Chrome/Chromium browser
### Installation Steps
1. **Download the latest Chrome extension from GitHub**
Download link: https://github.com/hangwin/mcp-chrome/releases
2. **Install mcp-chrome-bridge globally**
npm
```bash
npm install -g mcp-chrome-bridge
```
pnpm
```bash
# Method 1: Enable scripts globally (recommended)
pnpm config set enable-pre-post-scripts true
pnpm install -g mcp-chrome-bridge
# Method 2: Manual registration (if postinstall doesn't run)
pnpm install -g mcp-chrome-bridge
mcp-chrome-bridge register
```
> Note: pnpm v7+ disables postinstall scripts by default for security. The `enable-pre-post-scripts` setting controls whether pre/post install scripts run. If automatic registration fails, use the manual registration command above.
3. **Load Chrome Extension**
- Open Chrome and go to `chrome://extensions/`
- Enable "Developer mode"
- Click "Load unpacked" and select `your/dowloaded/extension/folder`
- Click the extension icon to open the plugin, then click connect to see the MCP configuration
<img width="475" alt="Screenshot 2025-06-09 15 52 06" src="https://github.com/user-attachments/assets/241e57b8-c55f-41a4-9188-0367293dc5bc" />
### Usage with MCP Protocol Clients
#### Using Streamable HTTP Connection (👍🏻 Recommended)
Add the following configuration to your MCP client configuration (using CherryStudio as an example):
> Streamable HTTP connection method is recommended
```json
{
"mcpServers": {
"chrome-mcp-server": {
"type": "streamableHttp",
"url": "http://127.0.0.1:12306/mcp"
}
}
}
```
#### Using STDIO Connection (Alternative)
If your client only supports stdio connection method, please use the following approach:
1. First, check the installation location of the npm package you just installed
```sh
# npm check method
npm list -g mcp-chrome-bridge
# pnpm check method
pnpm list -g mcp-chrome-bridge
```
Assuming the command above outputs the path: /Users/xxx/Library/pnpm/global/5
Then your final path would be: /Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js
2. Replace the configuration below with the final path you just obtained
```json
{
"mcpServers": {
"chrome-mcp-stdio": {
"command": "npx",
"args": [
"node",
"/Users/xxx/Library/pnpm/global/5/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js"
]
}
}
}
```
eg:config in augment:
<img width="494" alt="截屏2025-06-22 22 11 25" src="https://github.com/user-attachments/assets/48eefc0c-a257-4d3b-8bbe-d7ff716de2bf" />
## 🛠️ Available Tools
Complete tool list: [Complete Tool List](docs/TOOLS.md)
<details>
<summary><strong>📊 Browser Management (6 tools)</strong></summary>
- `get_windows_and_tabs` - List all browser windows and tabs
- `chrome_navigate` - Navigate to URLs and control viewport
- `chrome_switch_tab` - Switch the current active tab
- `chrome_close_tabs` - Close specific tabs or windows
- `chrome_go_back_or_forward` - Browser navigation control
- `chrome_inject_script` - Inject content scripts into web pages
- `chrome_send_command_to_inject_script` - Send commands to injected content scripts
</details>
<details>
<summary><strong>📸 Screenshots & Visual (1 tool)</strong></summary>
- `chrome_screenshot` - Advanced screenshot capture with element targeting, full-page support, and custom dimensions
</details>
<details>
<summary><strong>🌐 Network Monitoring (4 tools)</strong></summary>
- `chrome_network_capture_start/stop` - webRequest API network capture
- `chrome_network_debugger_start/stop` - Debugger API with response bodies
- `chrome_network_request` - Send custom HTTP requests
</details>
<details>
<summary><strong>🔍 Content Analysis (4 tools)</strong></summary>
- `search_tabs_content` - AI-powered semantic search across browser tabs
- `chrome_get_web_content` - Extract HTML/text content from pages
- `chrome_get_interactive_elements` - Find clickable elements
- `chrome_console` - Capture and retrieve console output from browser tabs
</details>
<details>
<summary><strong>🎯 Interaction (3 tools)</strong></summary>
- `chrome_click_element` - Click elements using CSS selectors
- `chrome_fill_or_select` - Fill forms and select options
- `chrome_keyboard` - Simulate keyboard input and shortcuts
</details>
<details>
<summary><strong>📚 Data Management (5 tools)</strong></summary>
- `chrome_history` - Search browser history with time filters
- `chrome_bookmark_search` - Find bookmarks by keywords
- `chrome_bookmark_add` - Add new bookmarks with folder support
- `chrome_bookmark_delete` - Delete bookmarks
</details>
## 🧪 Usage Examples
### AI helps you summarize webpage content and automatically control Excalidraw for drawing
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)
Instruction: Help me summarize the current page content, then draw a diagram to aid my understanding.
https://www.youtube.com/watch?v=3fBPdUBWVz0
https://github.com/user-attachments/assets/fd17209b-303d-48db-9e5e-3717141df183
### After analyzing the content of the image, the LLM automatically controls Excalidraw to replicate the image
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)|[content-analize](prompt/content-analize.md)
Instruction: First, analyze the content of the image, and then replicate the image by combining the analysis with the content of the image.
https://www.youtube.com/watch?v=tEPdHZBzbZk
https://github.com/user-attachments/assets/60d12b1a-9b74-40f4-994c-95e8fa1fc8d3
### AI automatically injects scripts and modifies webpage styles
prompt: [modify-web-prompt](prompt/modify-web.md)
Instruction: Help me modify the current page's style and remove advertisements.
https://youtu.be/twI6apRKHsk
https://github.com/user-attachments/assets/69cb561c-2e1e-4665-9411-4a3185f9643e
### AI automatically captures network requests for you
query: I want to know what the search API for Xiaohongshu is and what the response structure looks like
https://youtu.be/1hHKr7XKqnQ
https://github.com/user-attachments/assets/dc7e5cab-b9af-4b9a-97ce-18e4837318d9
### AI helps analyze your browsing history
query: Analyze my browsing history from the past month
https://youtu.be/jf2UZfrR2Vk
https://github.com/user-attachments/assets/31b2e064-88c6-4adb-96d7-50748b826eae
### Web page conversation
query: Translate and summarize the current web page
https://youtu.be/FlJKS9UQyC8
https://github.com/user-attachments/assets/aa8ef2a1-2310-47e6-897a-769d85489396
### AI automatically takes screenshots for you (web page screenshots)
query: Take a screenshot of Hugging Face's homepage
https://youtu.be/7ycK6iksWi4
https://github.com/user-attachments/assets/65c6eee2-6366-493d-a3bd-2b27529ff5b3
### AI automatically takes screenshots for you (element screenshots)
query: Capture the icon from Hugging Face's homepage
https://youtu.be/ev8VivANIrk
https://github.com/user-attachments/assets/d0cf9785-c2fe-4729-a3c5-7f2b8b96fe0c
### AI helps manage bookmarks
query: Add the current page to bookmarks and put it in an appropriate folder
https://youtu.be/R_83arKmFTo
https://github.com/user-attachments/assets/15a7d04c-0196-4b40-84c2-bafb5c26dfe0
### Automatically close web pages
query: Close all shadcn-related web pages
https://youtu.be/2wzUT6eNVg4
https://github.com/user-attachments/assets/83de4008-bb7e-494d-9b0f-98325cfea592
## 🤝 Contributing
We welcome contributions! Please see [CONTRIBUTING.md](docs/CONTRIBUTING.md) for detailed guidelines.
## 🚧 Future Roadmap
We have exciting plans for the future development of Chrome MCP Server:
- [ ] Authentication
- [ ] Recording and Playback
- [ ] Workflow Automation
- [ ] Enhanced Browser Support (Firefox Extension)
---
**Want to contribute to any of these features?** Check out our [Contributing Guide](docs/CONTRIBUTING.md) and join our development community!
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 📚 More Documentation
- [Architecture Design](docs/ARCHITECTURE.md) - Detailed technical architecture documentation
- [TOOLS API](docs/TOOLS.md) - Complete tool API documentation
- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issue solutions
```
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing Guide 🤝
Thank you for your interest in contributing to Chrome MCP Server! This document provides guidelines and information for contributors.
## 🎯 How to Contribute
We welcome contributions in many forms:
- 🐛 Bug reports and fixes
- ✨ New features and tools
- 📚 Documentation improvements
- 🧪 Tests and performance optimizations
- 🌐 Translations and internationalization
- 💡 Ideas and suggestions
## 🚀 Getting Started
### Prerequisites
- **Node.js 18.19.0+** and **pnpm or npm** (latest version)
- **Chrome/Chromium** browser for testing
- **Git** for version control
- **Rust** (for WASM development, optional)
- **TypeScript** knowledge
### Development Setup
1. **Fork and clone the repository**
```bash
git clone https://github.com/YOUR_USERNAME/chrome-mcp-server.git
cd chrome-mcp-server
```
2. **Install dependencies**
```bash
pnpm install
```
3. **Start the project**
```bash
npm run dev
```
4. **Load the extension in Chrome**
- Open `chrome://extensions/`
- Enable "Developer mode"
- Click "Load unpacked" and select `your/extension/dist`
## 🏗️ Project Structure
```
chrome-mcp-server/
├── app/
│ ├── chrome-extension/ # Chrome extension (WXT + Vue 3)
│ │ ├── entrypoints/ # Background scripts, popup, content scripts
│ │ ├── utils/ # AI models, vector database, utilities
│ │ └── workers/ # Web Workers for AI processing
│ └── native-server/ # Native messaging server (Fastify + TypeScript)
│ ├── src/mcp/ # MCP protocol implementation
│ └── src/server/ # HTTP server and native messaging
├── packages/
│ ├── shared/ # Shared types and utilities
│ └── wasm-simd/ # SIMD-optimized WebAssembly math functions
└── docs/ # Documentation
```
## 🛠️ Development Workflow
### Adding New Tools
1. **Define the tool schema in `packages/shared/src/tools.ts`**:
```typescript
{
name: 'your_new_tool',
description: 'Description of what your tool does',
inputSchema: {
type: 'object',
properties: {
// Define parameters
},
required: ['param1']
}
}
```
2. **Implement the tool in `app/chrome-extension/entrypoints/background/tools/browser/`**:
```typescript
class YourNewTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.YOUR_NEW_TOOL;
async execute(args: YourToolParams): Promise<ToolResult> {
// Implementation
}
}
```
3. **Export the tool in `app/chrome-extension/entrypoints/background/tools/browser/index.ts`**
4. **Add tests in the appropriate test directory**
### Code Style Guidelines
- **TypeScript**: Use strict TypeScript with proper typing
- **ESLint**: Follow the configured ESLint rules (`pnpm lint`)
- **Prettier**: Format code with Prettier (`pnpm format`)
- **Naming**: Use descriptive names and follow existing patterns
- **Comments**: Add JSDoc comments for public APIs
- **Error Handling**: Always handle errors gracefully
## 📝 Pull Request Process
1. **Create a feature branch**
```bash
git checkout -b feature/your-feature-name
```
2. **Make your changes**
- Follow the code style guidelines
- Add tests for new functionality
- Update documentation if needed
3. **Test your changes**
- Ensure all existing tests pass
- Test the Chrome extension manually
- Verify MCP protocol compatibility
4. **Commit your changes**
```bash
git add .
git commit -m "feat: add your feature description"
```
We use [Conventional Commits](https://www.conventionalcommits.org/):
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation changes
- `test:` for adding tests
- `refactor:` for code refactoring
5. **Push and create a Pull Request**
```bash
git push origin feature/your-feature-name
```
## 🐛 Bug Reports
When reporting bugs, please include:
- **Environment**: OS, Chrome version, Node.js version
- **Steps to reproduce**: Clear, step-by-step instructions
- **Expected behavior**: What should happen
- **Actual behavior**: What actually happens
- **Screenshots/logs**: If applicable
- **MCP client**: Which MCP client you're using (Claude Desktop, etc.)
## 💡 Feature Requests
For feature requests, please provide:
- **Use case**: Why is this feature needed?
- **Proposed solution**: How should it work?
- **Alternatives**: Any alternative solutions considered?
- **Additional context**: Screenshots, examples, etc.
## 🔧 Development Tips
### Using WASM SIMD
If you're contributing to the WASM SIMD package:
```bash
cd packages/wasm-simd
# Install Rust and wasm-pack if not already installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install wasm-pack
# Build WASM package
pnpm build
# The built files will be copied to app/chrome-extension/workers/
```
### Debugging Chrome Extension
- Use Chrome DevTools for debugging extension popup and background scripts
- Check `chrome://extensions/` for extension errors
- Use `console.log` statements for debugging
- Monitor the native messaging connection in the background script
### Testing MCP Protocol
- Use MCP Inspector for protocol debugging
- Test with different MCP clients (Claude Desktop, custom clients)
- Verify tool schemas and responses match MCP specifications
## 📚 Resources
- [Model Context Protocol Specification](https://modelcontextprotocol.io/)
- [Chrome Extension Development](https://developer.chrome.com/docs/extensions/)
- [WXT Framework Documentation](https://wxt.dev/)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
## 🤝 Community
- **GitHub Issues**: For bug reports and feature requests
- **GitHub Discussions**: For questions and general discussion
- **Pull Requests**: For code contributions
## 📄 License
By contributing to Chrome MCP Server, you agree that your contributions will be licensed under the MIT License.
## 🎯 Contributor Guidelines
### New Contributors
If you're contributing to an open source project for the first time:
1. **Start small**: Look for issues labeled "good first issue"
2. **Read the code**: Familiarize yourself with the project structure and coding style
3. **Ask questions**: Ask questions in GitHub Discussions
4. **Learn the tools**: Get familiar with Git, GitHub, TypeScript, and other tools
### Experienced Contributors
- **Architecture improvements**: Propose system-level improvements
- **Performance optimization**: Identify and fix performance bottlenecks
- **New features**: Design and implement complex new features
- **Mentor newcomers**: Help new contributors get started
### Documentation Contributions
- **API documentation**: Improve tool documentation and examples
- **Tutorials**: Create usage guides and best practices
- **Translations**: Help translate documentation to other languages
- **Video content**: Create demo videos and tutorials
### Testing Contributions
- **Unit tests**: Write tests for new features
- **Integration tests**: Test interactions between components
- **Performance tests**: Benchmark testing and performance regression detection
- **User testing**: Functional testing in real-world scenarios
## 🏆 Contributor Recognition
We value every contribution, no matter how big or small. Contributors will be recognized in the following ways:
- **README acknowledgments**: Contributors listed in the project README
- **Release notes**: Contributors thanked in version release notes
- **Contributor badges**: Contributor badges on GitHub profiles
- **Community recognition**: Special thanks in community discussions
Thank you for considering contributing to Chrome MCP Server! Your participation makes this project better.
```
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
```yaml
packages:
- 'app/*'
- 'packages/*'
```
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
```json
{
"recommendations": ["Vue.volar"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"extends": "./.wxt/tsconfig.json"
}
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/stdio-config.json:
--------------------------------------------------------------------------------
```json
{
"url": "http://127.0.0.1:12306/mcp"
}
```
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
```
module.exports = {
extends: ['@commitlint/config-conventional'],
};
```
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './constants';
export * from './types';
export * from './tools';
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/register-dev.ts:
--------------------------------------------------------------------------------
```typescript
import { tryRegisterUserLevelHost } from './utils';
tryRegisterUserLevelHost();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/content.ts:
--------------------------------------------------------------------------------
```typescript
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {},
});
```
--------------------------------------------------------------------------------
/packages/shared/src/constants.ts:
--------------------------------------------------------------------------------
```typescript
export const DEFAULT_SERVER_PORT = 56889;
export const HOST_NAME = 'com.chrome_mcp.native_host';
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/main.ts:
--------------------------------------------------------------------------------
```typescript
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';
createApp(App).mount('#app');
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/offscreen/index.html:
--------------------------------------------------------------------------------
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/constant.ts:
--------------------------------------------------------------------------------
```typescript
export const COMMAND_NAME = 'chrome-mcp-bridge';
export const EXTENSION_ID = 'hbdgbgagpkpjffpklnamcljpakneikee';
export const HOST_NAME = 'com.chromemcp.nativehost';
export const DESCRIPTION = 'Node.js Host for Browser Bridge Extension';
```
--------------------------------------------------------------------------------
/packages/wasm-simd/Cargo.toml:
--------------------------------------------------------------------------------
```toml
[package]
name = "simd-math"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
wide = "0.7"
console_error_panic_hook = "0.1"
[dependencies.web-sys]
version = "0.3"
features = [
"console",
]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
```
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Default Popup Title</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/index.ts:
--------------------------------------------------------------------------------
```typescript
export { default as DocumentIcon } from './DocumentIcon.vue';
export { default as DatabaseIcon } from './DatabaseIcon.vue';
export { default as BoltIcon } from './BoltIcon.vue';
export { default as TrashIcon } from './TrashIcon.vue';
export { default as CheckIcon } from './CheckIcon.vue';
export { default as TabIcon } from './TabIcon.vue';
export { default as VectorIcon } from './VectorIcon.vue';
```
--------------------------------------------------------------------------------
/app/native-server/jest.config.js:
--------------------------------------------------------------------------------
```javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/scripts/**/*'],
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
},
};
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { setupTools } from './register-tools';
export let mcpServer: Server | null = null;
export const getMcpServer = () => {
if (mcpServer) {
return mcpServer;
}
mcpServer = new Server(
{
name: 'ChromeMcpServer',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
},
);
setupTools(mcpServer);
return mcpServer;
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/BoltIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/assets/vue.svg:
--------------------------------------------------------------------------------
```
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
```
--------------------------------------------------------------------------------
/app/native-server/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2018",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2018", "DOM"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/CheckIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
:class="className"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.052-.143Z"
clip-rule="evenodd"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-small',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/tool-handler.ts:
--------------------------------------------------------------------------------
```typescript
import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js';
export interface ToolResult extends CallToolResult {
content: (TextContent | ImageContent)[];
isError: boolean;
}
export interface ToolExecutor {
execute(args: any): Promise<ToolResult>;
}
export const createErrorResponse = (
message: string = 'Unknown error, please try again',
): ToolResult => {
return {
content: [
{
type: 'text',
text: message,
},
],
isError: true,
};
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/DatabaseIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/VectorIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 4.5a4.5 4.5 0 0 1 6 0M9 4.5V3a1.5 1.5 0 0 1 1.5-1.5h3A1.5 1.5 0 0 1 15 3v1.5M9 4.5a4.5 4.5 0 0 0-4.5 4.5v7.5A1.5 1.5 0 0 0 6 18h12a1.5 1.5 0 0 0 1.5-1.5V9a4.5 4.5 0 0 0-4.5-4.5M12 12l2.25 2.25M12 12l-2.25-2.25M12 12v6"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/TabIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-16.5 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/DocumentIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/packages/shared/src/types.ts:
--------------------------------------------------------------------------------
```typescript
export enum NativeMessageType {
START = 'start',
STARTED = 'started',
STOP = 'stop',
STOPPED = 'stopped',
PING = 'ping',
PONG = 'pong',
ERROR = 'error',
PROCESS_DATA = 'process_data',
PROCESS_DATA_RESPONSE = 'process_data_response',
CALL_TOOL = 'call_tool',
CALL_TOOL_RESPONSE = 'call_tool_response',
// Additional message types used in Chrome extension
SERVER_STARTED = 'server_started',
SERVER_STOPPED = 'server_stopped',
ERROR_FROM_NATIVE_HOST = 'error_from_native_host',
CONNECT_NATIVE = 'connectNative',
PING_NATIVE = 'ping_native',
DISCONNECT_NATIVE = 'disconnect_native',
}
export interface NativeMessage<P = any, E = any> {
type?: NativeMessageType;
responseToRequestId?: string;
payload?: P;
error?: E;
}
```
--------------------------------------------------------------------------------
/packages/wasm-simd/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@chrome-mcp/wasm-simd",
"version": "0.1.0",
"description": "SIMD-optimized WebAssembly math functions for Chrome MCP",
"main": "pkg/simd_math.js",
"types": "pkg/simd_math.d.ts",
"files": [
"pkg/"
],
"scripts": {
"build": "wasm-pack build --target web --out-dir pkg --release",
"build:dev": "wasm-pack build --target web --out-dir pkg --dev",
"clean": "rimraf pkg/",
"test": "wasm-pack test --headless --firefox"
},
"keywords": [
"wasm",
"simd",
"webassembly",
"math",
"cosine-similarity",
"vector-operations"
],
"author": "hangye",
"license": "MIT",
"devDependencies": {
"rimraf": "^5.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/your-repo/chrome-mcp-server.git",
"directory": "packages/wasm-simd"
}
}
```
--------------------------------------------------------------------------------
/app/native-server/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import serverInstance from './server';
import nativeMessagingHostInstance from './native-messaging-host';
try {
serverInstance.setNativeHost(nativeMessagingHostInstance); // Server needs setNativeHost method
nativeMessagingHostInstance.setServer(serverInstance); // NativeHost needs setServer method
nativeMessagingHostInstance.start();
} catch (error) {
process.exit(1);
}
process.on('error', (error) => {
process.exit(1);
});
// Handle process signals and uncaught exceptions
process.on('SIGINT', () => {
process.exit(0);
});
process.on('SIGTERM', () => {
process.exit(0);
});
process.on('exit', (code) => {
});
process.on('uncaughtException', (error) => {
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
// Don't exit immediately, let the program continue running
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/icons/TrashIcon.vue:
--------------------------------------------------------------------------------
```vue
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
:class="className"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</template>
<script lang="ts" setup>
interface Props {
className?: string;
}
withDefaults(defineProps<Props>(), {
className: 'icon-default',
});
</script>
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse } from '@/common/tool-handler';
import { ERROR_MESSAGES } from '@/common/constants';
import * as browserTools from './browser';
const tools = { ...browserTools };
const toolsMap = new Map(Object.values(tools).map((tool) => [tool.name, tool]));
/**
* Tool call parameter interface
*/
export interface ToolCallParam {
name: string;
args: any;
}
/**
* Handle tool execution
*/
export const handleCallTool = async (param: ToolCallParam) => {
const tool = toolsMap.get(param.name);
if (!tool) {
return createErrorResponse(`Tool ${param.name} not found`);
}
try {
return await tool.execute(param.args);
} catch (error) {
console.error(`Tool execution failed for ${param.name}:`, error);
return createErrorResponse(
error instanceof Error ? error.message : ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
);
}
};
```
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "chrome-mcp-shared",
"version": "1.0.1",
"author": "hangye",
"main": "dist/index.js",
"module": "./dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "npx eslint 'src/**/*.{js,ts}'",
"lint:fix": "npx eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json}'"
},
"files": [
"dist"
],
"devDependencies": {
"@types/node": "^18.0.0",
"@typescript-eslint/parser": "^8.32.0",
"tsup": "^8.4.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"zod": "^3.24.4"
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/index.ts:
--------------------------------------------------------------------------------
```typescript
export { navigateTool, closeTabsTool, goBackOrForwardTool, switchTabTool } from './common';
export { windowTool } from './window';
export { vectorSearchTabsContentTool as searchTabsContentTool } from './vector-search';
export { screenshotTool } from './screenshot';
export { webFetcherTool, getInteractiveElementsTool } from './web-fetcher';
export { clickTool, fillTool } from './interaction';
export { networkRequestTool } from './network-request';
export { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';
export { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
export { keyboardTool } from './keyboard';
export { historyTool } from './history';
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
export { consoleTool } from './console';
export { fileUploadTool } from './file-upload';
```
--------------------------------------------------------------------------------
/app/chrome-extension/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "chrome-mcp-server",
"description": "a chrome extension to use your own chrome as a mcp server",
"author": "hangye",
"private": true,
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "vue-tsc --noEmit",
"postinstall": "wxt prepare",
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
"format": "npx prettier --write .",
"format:check": "npx prettier --check ."
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@xenova/transformers": "^2.17.2",
"chrome-mcp-shared": "workspace:*",
"date-fns": "^4.1.0",
"hnswlib-wasm-static": "0.8.5",
"vue": "^3.5.13",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/chrome": "^0.0.318",
"@wxt-dev/module-vue": "^1.0.2",
"dotenv": "^16.5.0",
"vite-plugin-static-copy": "^3.0.0",
"vue-tsc": "^2.2.8",
"wxt": "^0.20.0"
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/index.ts:
--------------------------------------------------------------------------------
```typescript
import { initNativeHostListener } from './native-host';
import {
initSemanticSimilarityListener,
initializeSemanticEngineIfCached,
} from './semantic-similarity';
import { initStorageManagerListener } from './storage-manager';
import { cleanupModelCache } from '@/utils/semantic-similarity-engine';
/**
* Background script entry point
* Initializes all background services and listeners
*/
export default defineBackground(() => {
// Initialize core services
initNativeHostListener();
initSemanticSimilarityListener();
initStorageManagerListener();
// Conditionally initialize semantic similarity engine if model cache exists
initializeSemanticEngineIfCached()
.then((initialized) => {
if (initialized) {
console.log('Background: Semantic similarity engine initialized from cache');
} else {
console.log(
'Background: Semantic similarity engine initialization skipped (no cache found)',
);
}
})
.catch((error) => {
console.warn('Background: Failed to conditionally initialize semantic engine:', error);
});
// Initial cleanup on startup
cleanupModelCache().catch((error) => {
console.warn('Background: Initial cache cleanup failed:', error);
});
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import { defineConfig } from 'eslint/config';
import prettierConfig from 'eslint-config-prettier';
export default defineConfig([
// Global ignores - these apply to all configurations
{
ignores: [
'dist/**',
'.output/**',
'.wxt/**',
'node_modules/**',
'logs/**',
'*.log',
'.cache/**',
'.temp/**',
'.vscode/**',
'!.vscode/extensions.json',
'.idea/**',
'.DS_Store',
'Thumbs.db',
'*.zip',
'*.tar.gz',
'stats.html',
'stats-*.json',
'libs/**',
'workers/**',
'public/libs/**',
],
},
js.configs.recommended,
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: { globals: globals.browser },
},
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
pluginVue.configs['flat/essential'],
{ files: ['**/*.vue'], languageOptions: { parserOptions: { parser: tseslint.parser } } },
// Prettier configuration - must be placed last to override previous rules
prettierConfig,
]);
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import globals from 'globals';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
// Global ignores first - these apply to all configurations
{
ignores: [
'node_modules/',
'dist/',
'.output/',
'.wxt/',
'logs/',
'*.log',
'.cache/',
'.temp/',
'.idea/',
'.DS_Store',
'Thumbs.db',
'*.zip',
'*.tar.gz',
'stats.html',
'stats-*.json',
'pnpm-lock.yaml',
'**/workers/**',
'app/**/workers/**',
'packages/**/workers/**',
'test-inject-script.js',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['app/**/*.{js,jsx,ts,tsx}', 'packages/**/*.{js,jsx,ts,tsx}'],
ignores: ['**/workers/**'], // Additional ignores for this specific config
languageOptions: {
ecmaVersion: 2021,
sourceType: 'module',
parser: tseslint.parser,
globals: {
...globals.node,
...globals.es2021,
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
eslintConfigPrettier,
);
```
--------------------------------------------------------------------------------
/app/native-server/src/constant/index.ts:
--------------------------------------------------------------------------------
```typescript
export enum NATIVE_MESSAGE_TYPE {
START = 'start',
STARTED = 'started',
STOP = 'stop',
STOPPED = 'stopped',
PING = 'ping',
PONG = 'pong',
ERROR = 'error',
}
export const NATIVE_SERVER_PORT = 56889;
// Timeout constants (in milliseconds)
export const TIMEOUTS = {
DEFAULT_REQUEST_TIMEOUT: 15000,
EXTENSION_REQUEST_TIMEOUT: 20000,
PROCESS_DATA_TIMEOUT: 20000,
} as const;
// Server configuration
export const SERVER_CONFIG = {
HOST: '127.0.0.1',
CORS_ORIGIN: true,
LOGGER_ENABLED: false,
} as const;
// HTTP Status codes
export const HTTP_STATUS = {
OK: 200,
NO_CONTENT: 204,
BAD_REQUEST: 400,
INTERNAL_SERVER_ERROR: 500,
GATEWAY_TIMEOUT: 504,
} as const;
// Error messages
export const ERROR_MESSAGES = {
NATIVE_HOST_NOT_AVAILABLE: 'Native host connection not established.',
SERVER_NOT_RUNNING: 'Server is not actively running.',
REQUEST_TIMEOUT: 'Request to extension timed out.',
INVALID_MCP_REQUEST: 'Invalid MCP request or session.',
INVALID_SESSION_ID: 'Invalid or missing MCP session ID.',
INTERNAL_SERVER_ERROR: 'Internal Server Error',
MCP_SESSION_DELETION_ERROR: 'Internal server error during MCP session deletion.',
MCP_REQUEST_PROCESSING_ERROR: 'Internal server error during MCP request processing.',
INVALID_SSE_SESSION: 'Invalid or missing MCP session ID for SSE.',
} as const;
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/register-tools.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
CallToolResult,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import nativeMessagingHostInstance from '../native-messaging-host';
import { NativeMessageType, TOOL_SCHEMAS } from 'chrome-mcp-shared';
export const setupTools = (server: Server) => {
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments || {}),
);
};
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
try {
// 发送请求到Chrome扩展并等待响应
const response = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
{
name,
args,
},
NativeMessageType.CALL_TOOL,
30000, // 30秒超时
);
if (response.status === 'success') {
return response.data;
} else {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${response.error}`,
},
],
isError: true,
};
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${error.message}`,
},
],
isError: true,
};
}
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/window.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
class WindowTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS;
async execute(): Promise<ToolResult> {
try {
const windows = await chrome.windows.getAll({ populate: true });
let tabCount = 0;
const structuredWindows = windows.map((window) => {
const tabs =
window.tabs?.map((tab) => {
tabCount++;
return {
tabId: tab.id || 0,
url: tab.url || '',
title: tab.title || '',
active: tab.active || false,
};
}) || [];
return {
windowId: window.id || 0,
tabs: tabs,
};
});
const result = {
windowCount: windows.length,
tabCount: tabCount,
windows: structuredWindows,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in WindowTool.execute:', error);
return createErrorResponse(
`Error getting windows and tabs information: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const windowTool = new WindowTool();
```
--------------------------------------------------------------------------------
/docs/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------
```markdown
# 🚀 Installation and Connection Issues
### If Connection Fails After Clicking the Connect Button on the Extension
1. **Check if mcp-chrome-bridge is installed successfully**, ensure it's globally installed
```bash
mcp-chrome-bridge -V
```
<img width="612" alt="Screenshot 2025-06-11 15 09 57" src="https://github.com/user-attachments/assets/59458532-e6e1-457c-8c82-3756a5dbb28e" />
2. **Check if the manifest file is in the correct directory**
Windows path: C:\Users\xxx\AppData\Roaming\Google\Chrome\NativeMessagingHosts
Mac path: /Users/xxx/Library/Application\ Support/Google/Chrome/NativeMessagingHosts
If the npm package is installed correctly, a file named `com.chromemcp.nativehost.json` should be generated in this directory
3. **Check if there are logs in the npm package installation directory**
You need to check your installation path (if unclear, open the manifest file in step 2, the path field shows the installation directory). For example, if the installation path is as follows, check the log contents:
C:\Users\admin\AppData\Local\nvm\v20.19.2\node_modules\mcp-chrome-bridge\dist\logs
<img width="804" alt="Screenshot 2025-06-11 15 09 41" src="https://github.com/user-attachments/assets/ce7b7c94-7c84-409a-8210-c9317823aae1" />
4. **Check if you have execution permissions**
You need to check your installation path (if unclear, open the manifest file in step 2, the path field shows the installation directory). For example, if the Mac installation path is as follows:
`xxx/node_modules/mcp-chrome-bridge/dist/run_host.sh`
Check if this script has execution permissions
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ProgressIndicator.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div v-if="visible" class="progress-section">
<div class="progress-indicator">
<div class="spinner" v-if="showSpinner"></div>
<span class="progress-text">{{ text }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
interface Props {
visible?: boolean;
text: string;
showSpinner?: boolean;
}
withDefaults(defineProps<Props>(), {
visible: true,
showSpinner: true,
});
</script>
<style scoped>
.progress-section {
margin-top: 16px;
animation: slideIn 0.3s ease-out;
}
.progress-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 8px;
border-left: 4px solid #667eea;
backdrop-filter: blur(10px);
border: 1px solid rgba(102, 126, 234, 0.2);
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(102, 126, 234, 0.2);
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.progress-text {
font-size: 14px;
color: #4a5568;
font-weight: 500;
line-height: 1.4;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 420px) {
.progress-indicator {
padding: 12px;
gap: 8px;
}
.spinner {
width: 16px;
height: 16px;
border-width: 2px;
}
.progress-text {
font-size: 13px;
}
}
</style>
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-chrome-bridge-monorepo",
"version": "1.0.0",
"private": true,
"author": "hangye",
"type": "module",
"scripts": {
"build:shared": "pnpm --filter chrome-mcp-shared build",
"build:native": "pnpm --filter mcp-chrome-bridge build",
"build:extension": "pnpm --filter chrome-mcp-server build",
"build:wasm": "pnpm --filter @chrome-mcp/wasm-simd build && pnpm run copy:wasm",
"build": "pnpm -r --filter='!@chrome-mcp/wasm-simd' build",
"copy:wasm": "cp ./packages/wasm-simd/pkg/simd_math.js ./packages/wasm-simd/pkg/simd_math_bg.wasm ./app/chrome-extension/workers/",
"dev:shared": "pnpm --filter chrome-mcp-shared dev",
"dev:native": "pnpm --filter mcp-chrome-bridge dev",
"dev:extension": "pnpm --filter chrome-mcp-server dev",
"dev": "pnpm --filter chrome-mcp-shared build && pnpm -r --parallel dev",
"lint": "pnpm -r lint",
"lint:fix": "pnpm -r lint:fix",
"format": "pnpm -r format",
"clean:dist": "pnpm -r exec rm -rf dist .turbo",
"clean:modules": "pnpm -r exec rm -rf node_modules && rm -rf node_modules",
"clean": "npm run clean:dist && npm run clean:modules",
"typecheck": "pnpm -r exec tsc --noEmit",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@eslint/js": "^9.25.1",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^10.0.0",
"globals": "^16.1.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
"vue-eslint-parser": "^10.1.3"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,vue}": [
"eslint --fix",
"prettier --write"
],
"**/*.{json,md,yaml,html,css}": [
"prettier --write"
]
}
}
```
--------------------------------------------------------------------------------
/app/native-server/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-chrome-bridge",
"version": "1.0.29",
"description": "Chrome Native-Messaging host (Node)",
"main": "dist/index.js",
"bin": {
"mcp-chrome-bridge": "./dist/cli.js",
"mcp-chrome-stdio": "./dist/mcp/mcp-server-stdio.js"
},
"scripts": {
"dev": "nodemon --watch src --ext ts,js,json --ignore dist/ --exec \"npm run build && npm run register:dev\"",
"build": "ts-node src/scripts/build.ts",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint 'src/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json}'",
"register:dev": "node dist/scripts/register-dev.js",
"postinstall": "node dist/scripts/postinstall.js"
},
"files": [
"dist"
],
"engines": {
"node": ">=14.0.0"
},
"preferGlobal": true,
"keywords": [
"mcp",
"chrome",
"browser"
],
"author": "hangye",
"license": "MIT",
"dependencies": {
"@fastify/cors": "^11.0.1",
"@modelcontextprotocol/sdk": "^1.11.0",
"@types/node-fetch": "^2.6.13",
"chalk": "^5.4.1",
"chrome-mcp-shared": "workspace:*",
"commander": "^13.1.0",
"fastify": "^5.3.2",
"is-admin": "^4.0.0",
"node-fetch": "^2.7.0",
"pino": "^9.6.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/chrome": "^0.0.318",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
"@types/supertest": "^6.0.3",
"@typescript-eslint/parser": "^8.31.1",
"cross-env": "^7.0.3",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^15.5.1",
"nodemon": "^3.1.10",
"pino-pretty": "^13.0.0",
"rimraf": "^6.0.1",
"supertest": "^7.1.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/build-release.yml:
--------------------------------------------------------------------------------
```yaml
# name: Build and Release Chrome Extension
# on:
# push:
# branches: [ master, develop ]
# paths:
# - 'app/chrome-extension/**'
# pull_request:
# branches: [ master ]
# paths:
# - 'app/chrome-extension/**'
# workflow_dispatch:
# jobs:
# build-extension:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Setup Node.js
# uses: actions/setup-node@v4
# with:
# node-version: '18'
# cache: 'npm'
# cache-dependency-path: 'app/chrome-extension/package-lock.json'
# - name: Install dependencies
# run: |
# cd app/chrome-extension
# npm ci
# - name: Build extension
# run: |
# cd app/chrome-extension
# npm run build
# - name: Create zip package
# run: |
# cd app/chrome-extension
# npm run zip
# - name: Prepare release directory
# run: |
# mkdir -p releases/chrome-extension/latest
# mkdir -p releases/chrome-extension/$(date +%Y%m%d-%H%M%S)
# - name: Copy release files
# run: |
# # Copy to latest
# cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/latest/chrome-mcp-server-latest.zip
# # Copy to timestamped version
# TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/$TIMESTAMP/chrome-mcp-server-$TIMESTAMP.zip
# - name: Upload build artifacts
# uses: actions/upload-artifact@v4
# with:
# name: chrome-extension-build
# path: releases/chrome-extension/
# retention-days: 30
# - name: Commit and push releases (if on main branch)
# if: github.ref == 'refs/heads/main'
# run: |
# git config --local user.email "[email protected]"
# git config --local user.name "GitHub Action"
# git add releases/
# git diff --staged --quiet || git commit -m "Auto-build: Update Chrome extension release [skip ci]"
# git push
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/inject-bridge.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
(() => {
// Prevent duplicate injection of the bridge itself.
if (window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__) return;
window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__ = true;
const EVENT_NAME = {
RESPONSE: 'chrome-mcp:response',
CLEANUP: 'chrome-mcp:cleanup',
EXECUTE: 'chrome-mcp:execute',
};
const pendingRequests = new Map();
const messageHandler = (request, _sender, sendResponse) => {
// --- Lifecycle Command ---
if (request.type === EVENT_NAME.CLEANUP) {
window.dispatchEvent(new CustomEvent(EVENT_NAME.CLEANUP));
// Acknowledge cleanup signal received, but don't hold the connection.
sendResponse({ success: true });
return true;
}
// --- Execution Command for MAIN world ---
if (request.targetWorld === 'MAIN') {
const requestId = `req-${Date.now()}-${Math.random()}`;
pendingRequests.set(requestId, sendResponse);
window.dispatchEvent(
new CustomEvent(EVENT_NAME.EXECUTE, {
detail: {
action: request.action,
payload: request.payload,
requestId: requestId,
},
}),
);
return true; // Async response is expected.
}
// Note: Requests for ISOLATED world are handled by the user's isolatedWorldCode script directly.
// This listener won't process them unless it's the only script in ISOLATED world.
};
chrome.runtime.onMessage.addListener(messageHandler);
// Listen for responses coming back from the MAIN world.
const responseHandler = (event) => {
const { requestId, data, error } = event.detail;
if (pendingRequests.has(requestId)) {
const sendResponse = pendingRequests.get(requestId);
sendResponse({ data, error });
pendingRequests.delete(requestId);
}
};
window.addEventListener(EVENT_NAME.RESPONSE, responseHandler);
// --- Self Cleanup ---
// When the cleanup signal arrives, this bridge must also clean itself up.
const cleanupHandler = () => {
chrome.runtime.onMessage.removeListener(messageHandler);
window.removeEventListener(EVENT_NAME.RESPONSE, responseHandler);
window.removeEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
delete window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__;
};
window.addEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
})();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/keyboard.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
interface KeyboardToolParams {
keys: string; // Required: string representing keys or key combinations to simulate (e.g., "Enter", "Ctrl+C")
selector?: string; // Optional: CSS selector for target element to send keyboard events to
delay?: number; // Optional: delay between keystrokes in milliseconds
}
/**
* Tool for simulating keyboard input on web pages
*/
class KeyboardTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.KEYBOARD;
/**
* Execute keyboard operation
*/
async execute(args: KeyboardToolParams): Promise<ToolResult> {
const { keys, selector, delay = TIMEOUTS.KEYBOARD_DELAY } = args;
console.log(`Starting keyboard operation with options:`, args);
if (!keys) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Keys parameter must be provided',
);
}
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/keyboard-helper.js']);
// Send keyboard simulation message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.SIMULATE_KEYBOARD,
keys,
selector,
delay,
});
if (result.error) {
return createErrorResponse(result.error);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Keyboard operation successful',
targetElement: result.targetElement,
results: result.results,
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in keyboard operation:', error);
return createErrorResponse(
`Error simulating keyboard events: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const keyboardTool = new KeyboardTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/network-request.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 30000; // For sending a single request via content script
interface NetworkRequestToolParams {
url: string; // URL is always required
method?: string; // Defaults to GET
headers?: Record<string, string>; // User-provided headers
body?: any; // User-provided body
timeout?: number; // Timeout for the network request itself
}
/**
* NetworkRequestTool - Sends network requests based on provided parameters.
*/
class NetworkRequestTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.NETWORK_REQUEST;
async execute(args: NetworkRequestToolParams): Promise<ToolResult> {
const {
url,
method = 'GET',
headers = {},
body,
timeout = DEFAULT_NETWORK_REQUEST_TIMEOUT,
} = args;
console.log(`NetworkRequestTool: Executing with options:`, args);
if (!url) {
return createErrorResponse('URL parameter is required.');
}
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse('No active tab found or tab has no ID.');
}
const activeTabId = tabs[0].id;
// Ensure content script is available in the target tab
await this.injectContentScript(activeTabId, ['inject-scripts/network-helper.js']);
console.log(
`NetworkRequestTool: Sending to content script: URL=${url}, Method=${method}, Headers=${Object.keys(headers).join(',')}, BodyType=${typeof body}`,
);
const resultFromContentScript = await this.sendMessageToTab(activeTabId, {
action: TOOL_MESSAGE_TYPES.NETWORK_SEND_REQUEST,
url: url,
method: method,
headers: headers,
body: body,
timeout: timeout,
});
console.log(`NetworkRequestTool: Response from content script:`, resultFromContentScript);
return {
content: [
{
type: 'text',
text: JSON.stringify(resultFromContentScript),
},
],
isError: !resultFromContentScript?.success,
};
} catch (error: any) {
console.error('NetworkRequestTool: Error sending network request:', error);
return createErrorResponse(
`Error sending network request: ${error.message || String(error)}`,
);
}
}
}
export const networkRequestTool = new NetworkRequestTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/offscreen-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Offscreen Document manager
* Ensures only one offscreen document is created across the entire extension to avoid conflicts
*/
export class OffscreenManager {
private static instance: OffscreenManager | null = null;
private isCreated = false;
private isCreating = false;
private createPromise: Promise<void> | null = null;
private constructor() {}
/**
* Get singleton instance
*/
public static getInstance(): OffscreenManager {
if (!OffscreenManager.instance) {
OffscreenManager.instance = new OffscreenManager();
}
return OffscreenManager.instance;
}
/**
* Ensure offscreen document exists
*/
public async ensureOffscreenDocument(): Promise<void> {
if (this.isCreated) {
return;
}
if (this.isCreating && this.createPromise) {
return this.createPromise;
}
this.isCreating = true;
this.createPromise = this._doCreateOffscreenDocument().finally(() => {
this.isCreating = false;
});
return this.createPromise;
}
private async _doCreateOffscreenDocument(): Promise<void> {
try {
if (!chrome.offscreen) {
throw new Error('Offscreen API not available. Chrome 109+ required.');
}
const existingContexts = await (chrome.runtime as any).getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
});
if (existingContexts && existingContexts.length > 0) {
console.log('OffscreenManager: Offscreen document already exists');
this.isCreated = true;
return;
}
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['WORKERS'],
justification: 'Need to run semantic similarity engine with workers',
});
this.isCreated = true;
console.log('OffscreenManager: Offscreen document created successfully');
} catch (error) {
console.error('OffscreenManager: Failed to create offscreen document:', error);
this.isCreated = false;
throw error;
}
}
/**
* Check if offscreen document is created
*/
public isOffscreenDocumentCreated(): boolean {
return this.isCreated;
}
/**
* Close offscreen document
*/
public async closeOffscreenDocument(): Promise<void> {
try {
if (chrome.offscreen && this.isCreated) {
await chrome.offscreen.closeDocument();
this.isCreated = false;
console.log('OffscreenManager: Offscreen document closed');
}
} catch (error) {
console.error('OffscreenManager: Failed to close offscreen document:', error);
}
}
/**
* Reset state (for testing)
*/
public reset(): void {
this.isCreated = false;
this.isCreating = false;
this.createPromise = null;
}
}
export const offscreenManager = OffscreenManager.getInstance();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/base-browser.ts:
--------------------------------------------------------------------------------
```typescript
import { ToolExecutor } from '@/common/tool-handler';
import type { ToolResult } from '@/common/tool-handler';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
const PING_TIMEOUT_MS = 300;
/**
* Base class for browser tool executors
*/
export abstract class BaseBrowserToolExecutor implements ToolExecutor {
abstract name: string;
abstract execute(args: any): Promise<ToolResult>;
/**
* Inject content script into tab
*/
protected async injectContentScript(
tabId: number,
files: string[],
injectImmediately = false,
world: 'MAIN' | 'ISOLATED' = 'ISOLATED',
): Promise<void> {
console.log(`Injecting ${files.join(', ')} into tab ${tabId}`);
// check if script is already injected
try {
const response = await Promise.race([
chrome.tabs.sendMessage(tabId, { action: `${this.name}_ping` }),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error(`${this.name} Ping action to tab ${tabId} timed out`)),
PING_TIMEOUT_MS,
),
),
]);
if (response && response.status === 'pong') {
console.log(
`pong received for action '${this.name}' in tab ${tabId}. Assuming script is active.`,
);
return;
} else {
console.warn(`Unexpected ping response in tab ${tabId}:`, response);
}
} catch (error) {
console.error(
`ping content script failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
try {
await chrome.scripting.executeScript({
target: { tabId },
files,
injectImmediately,
world,
});
console.log(`'${files.join(', ')}' injection successful for tab ${tabId}`);
} catch (injectionError) {
const errorMessage =
injectionError instanceof Error ? injectionError.message : String(injectionError);
console.error(
`Content script '${files.join(', ')}' injection failed for tab ${tabId}: ${errorMessage}`,
);
throw new Error(
`${ERROR_MESSAGES.TOOL_EXECUTION_FAILED}: Failed to inject content script in tab ${tabId}: ${errorMessage}`,
);
}
}
/**
* Send message to tab
*/
protected async sendMessageToTab(tabId: number, message: any): Promise<any> {
try {
const response = await chrome.tabs.sendMessage(tabId, message);
if (response && response.error) {
throw new Error(String(response.error));
}
return response;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(
`Error sending message to tab ${tabId} for action ${message?.action || 'unknown'}: ${errorMessage}`,
);
if (error instanceof Error) {
throw error;
}
throw new Error(errorMessage);
}
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/constants.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Chrome Extension Constants
* Centralized configuration values and magic constants
*/
// Native Host Configuration
export const NATIVE_HOST = {
NAME: 'com.chromemcp.nativehost',
DEFAULT_PORT: 12306,
} as const;
// Chrome Extension Icons
export const ICONS = {
NOTIFICATION: 'icon/48.png',
} as const;
// Timeouts and Delays (in milliseconds)
export const TIMEOUTS = {
DEFAULT_WAIT: 1000,
NETWORK_CAPTURE_MAX: 30000,
NETWORK_CAPTURE_IDLE: 3000,
SCREENSHOT_DELAY: 100,
KEYBOARD_DELAY: 50,
CLICK_DELAY: 100,
} as const;
// Limits and Thresholds
export const LIMITS = {
MAX_NETWORK_REQUESTS: 100,
MAX_SEARCH_RESULTS: 50,
MAX_BOOKMARK_RESULTS: 100,
MAX_HISTORY_RESULTS: 100,
SIMILARITY_THRESHOLD: 0.1,
VECTOR_DIMENSIONS: 384,
} as const;
// Error Messages
export const ERROR_MESSAGES = {
NATIVE_CONNECTION_FAILED: 'Failed to connect to native host',
NATIVE_DISCONNECTED: 'Native connection disconnected',
SERVER_STATUS_LOAD_FAILED: 'Failed to load server status',
SERVER_STATUS_SAVE_FAILED: 'Failed to save server status',
TOOL_EXECUTION_FAILED: 'Tool execution failed',
INVALID_PARAMETERS: 'Invalid parameters provided',
PERMISSION_DENIED: 'Permission denied',
TAB_NOT_FOUND: 'Tab not found',
ELEMENT_NOT_FOUND: 'Element not found',
NETWORK_ERROR: 'Network error occurred',
} as const;
// Success Messages
export const SUCCESS_MESSAGES = {
TOOL_EXECUTED: 'Tool executed successfully',
CONNECTION_ESTABLISHED: 'Connection established',
SERVER_STARTED: 'Server started successfully',
SERVER_STOPPED: 'Server stopped successfully',
} as const;
// File Extensions and MIME Types
export const FILE_TYPES = {
STATIC_EXTENSIONS: [
'.css',
'.js',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.ico',
'.woff',
'.woff2',
'.ttf',
],
FILTERED_MIME_TYPES: ['text/html', 'text/css', 'text/javascript', 'application/javascript'],
IMAGE_FORMATS: ['png', 'jpeg', 'webp'] as const,
} as const;
// Network Filtering
export const NETWORK_FILTERS = {
EXCLUDED_DOMAINS: [
'google-analytics.com',
'googletagmanager.com',
'facebook.com',
'doubleclick.net',
'googlesyndication.com',
],
STATIC_RESOURCE_TYPES: ['stylesheet', 'image', 'font', 'media', 'other'],
} as const;
// Semantic Similarity Configuration
export const SEMANTIC_CONFIG = {
DEFAULT_MODEL: 'sentence-transformers/all-MiniLM-L6-v2',
CHUNK_SIZE: 512,
CHUNK_OVERLAP: 50,
BATCH_SIZE: 32,
CACHE_SIZE: 1000,
} as const;
// Storage Keys
export const STORAGE_KEYS = {
SERVER_STATUS: 'serverStatus',
SEMANTIC_MODEL: 'selectedModel',
USER_PREFERENCES: 'userPreferences',
VECTOR_INDEX: 'vectorIndex',
} as const;
// Notification Configuration
export const NOTIFICATIONS = {
PRIORITY: 2,
TYPE: 'basic' as const,
} as const;
export enum ExecutionWorld {
ISOLATED = 'ISOLATED',
MAIN = 'MAIN',
}
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/run_host.bat:
--------------------------------------------------------------------------------
```
@echo off
setlocal enabledelayedexpansion
REM Setup paths
set "SCRIPT_DIR=%~dp0"
if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "LOG_DIR=%SCRIPT_DIR%\logs"
set "NODE_SCRIPT=%SCRIPT_DIR%\index.js"
if not exist "%LOG_DIR%" md "%LOG_DIR%"
REM Generate timestamp
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format 'yyyyMMdd_HHmmss'"') do set "TIMESTAMP=%%i"
set "WRAPPER_LOG=%LOG_DIR%\native_host_wrapper_windows_%TIMESTAMP%.log"
set "STDERR_LOG=%LOG_DIR%\native_host_stderr_windows_%TIMESTAMP%.log"
REM Initial logging
echo Wrapper script called at %DATE% %TIME% > "%WRAPPER_LOG%"
echo SCRIPT_DIR: %SCRIPT_DIR% >> "%WRAPPER_LOG%"
echo LOG_DIR: %LOG_DIR% >> "%WRAPPER_LOG%"
echo NODE_SCRIPT: %NODE_SCRIPT% >> "%WRAPPER_LOG%"
echo Initial PATH: %PATH% >> "%WRAPPER_LOG%"
echo User: %USERNAME% >> "%WRAPPER_LOG%"
echo Current PWD: %CD% >> "%WRAPPER_LOG%"
REM Node.js discovery
set "NODE_EXEC="
REM Priority 1: Installation-time node path
set "NODE_PATH_FILE=%SCRIPT_DIR%\node_path.txt"
echo Checking installation-time node path >> "%WRAPPER_LOG%"
if exist "%NODE_PATH_FILE%" (
set /p EXPECTED_NODE=<"%NODE_PATH_FILE%"
if exist "!EXPECTED_NODE!" (
set "NODE_EXEC=!EXPECTED_NODE!"
echo Found installation-time node at !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Priority 1.5: Fallback to relative path
if not defined NODE_EXEC (
set "EXPECTED_NODE=%SCRIPT_DIR%\..\..\..\node.exe"
echo Checking relative path >> "%WRAPPER_LOG%"
if exist "%EXPECTED_NODE%" (
set "NODE_EXEC=%EXPECTED_NODE%"
echo Found node at relative path: !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Priority 2: where command
if not defined NODE_EXEC (
echo Trying 'where node.exe' >> "%WRAPPER_LOG%"
for /f "delims=" %%i in ('where node.exe 2^>nul') do (
if not defined NODE_EXEC (
set "NODE_EXEC=%%i"
echo Found node using 'where': !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
)
REM Priority 3: Common paths
if not defined NODE_EXEC (
if exist "%ProgramFiles%\nodejs\node.exe" (
set "NODE_EXEC=%ProgramFiles%\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
) else if exist "%ProgramFiles(x86)%\nodejs\node.exe" (
set "NODE_EXEC=%ProgramFiles(x86)%\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
) else if exist "%LOCALAPPDATA%\Programs\nodejs\node.exe" (
set "NODE_EXEC=%LOCALAPPDATA%\Programs\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Validation
if not defined NODE_EXEC (
echo ERROR: Node.js executable not found! >> "%WRAPPER_LOG%"
exit /B 1
)
echo Using Node executable: %NODE_EXEC% >> "%WRAPPER_LOG%"
call "%NODE_EXEC%" -v >> "%WRAPPER_LOG%" 2>>&1
if not exist "%NODE_SCRIPT%" (
echo ERROR: Node.js script not found at %NODE_SCRIPT% >> "%WRAPPER_LOG%"
exit /B 1
)
echo Executing: "%NODE_EXEC%" "%NODE_SCRIPT%" >> "%WRAPPER_LOG%"
call "%NODE_EXEC%" "%NODE_SCRIPT%" 2>> "%STDERR_LOG%"
set "EXIT_CODE=%ERRORLEVEL%"
echo Exit code: %EXIT_CODE% >> "%WRAPPER_LOG%"
endlocal
exit /B %EXIT_CODE%
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/lru-cache.ts:
--------------------------------------------------------------------------------
```typescript
class LRUNode<K, V> {
constructor(
public key: K,
public value: V,
public prev: LRUNode<K, V> | null = null,
public next: LRUNode<K, V> | null = null,
public frequency: number = 1,
public lastAccessed: number = Date.now(),
) {}
}
class LRUCache<K = string, V = any> {
private capacity: number;
private cache: Map<K, LRUNode<K, V>>;
private head: LRUNode<K, V>;
private tail: LRUNode<K, V>;
constructor(capacity: number) {
this.capacity = capacity > 0 ? capacity : 100;
this.cache = new Map<K, LRUNode<K, V>>();
this.head = new LRUNode<K, V>(null as any, null as any);
this.tail = new LRUNode<K, V>(null as any, null as any);
this.head.next = this.tail;
this.tail.prev = this.head;
}
private addToHead(node: LRUNode<K, V>): void {
node.prev = this.head;
node.next = this.head.next;
this.head.next!.prev = node;
this.head.next = node;
}
private removeNode(node: LRUNode<K, V>): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private moveToHead(node: LRUNode<K, V>): void {
this.removeNode(node);
this.addToHead(node);
}
private findVictimNode(): LRUNode<K, V> {
let victim = this.tail.prev!;
let minScore = this.calculateEvictionScore(victim);
let current = this.tail.prev;
let count = 0;
const maxCheck = Math.min(5, this.cache.size);
while (current && current !== this.head && count < maxCheck) {
const score = this.calculateEvictionScore(current);
if (score < minScore) {
minScore = score;
victim = current;
}
current = current.prev;
count++;
}
return victim;
}
private calculateEvictionScore(node: LRUNode<K, V>): number {
const now = Date.now();
const timeSinceAccess = now - node.lastAccessed;
const timeWeight = 1 / (1 + timeSinceAccess / (1000 * 60));
const frequencyWeight = Math.log(node.frequency + 1);
return frequencyWeight * timeWeight;
}
get(key: K): V | null {
const node = this.cache.get(key);
if (node) {
node.frequency++;
node.lastAccessed = Date.now();
this.moveToHead(node);
return node.value;
}
return null;
}
set(key: K, value: V): void {
const existingNode = this.cache.get(key);
if (existingNode) {
existingNode.value = value;
this.moveToHead(existingNode);
} else {
const newNode = new LRUNode(key, value);
if (this.cache.size >= this.capacity) {
const victimNode = this.findVictimNode();
this.removeNode(victimNode);
this.cache.delete(victimNode.key);
}
this.cache.set(key, newNode);
this.addToHead(newNode);
}
}
has(key: K): boolean {
return this.cache.has(key);
}
clear(): void {
this.cache.clear();
this.head.next = this.tail;
this.tail.prev = this.head;
}
get size(): number {
return this.cache.size;
}
/**
* Get cache statistics
*/
getStats(): { size: number; capacity: number; usage: number } {
return {
size: this.cache.size,
capacity: this.capacity,
usage: this.cache.size / this.capacity,
};
}
}
export default LRUCache;
```
--------------------------------------------------------------------------------
/app/native-server/src/mcp/mcp-server-stdio.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
CallToolRequestSchema,
CallToolResult,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { TOOL_SCHEMAS } from 'chrome-mcp-shared';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import * as fs from 'fs';
import * as path from 'path';
let stdioMcpServer: Server | null = null;
let mcpClient: Client | null = null;
// Read configuration from stdio-config.json
const loadConfig = () => {
try {
const configPath = path.join(__dirname, 'stdio-config.json');
const configData = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configData);
} catch (error) {
console.error('Failed to load stdio-config.json:', error);
throw new Error('Configuration file stdio-config.json not found or invalid');
}
};
export const getStdioMcpServer = () => {
if (stdioMcpServer) {
return stdioMcpServer;
}
stdioMcpServer = new Server(
{
name: 'StdioChromeMcpServer',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
},
);
setupTools(stdioMcpServer);
return stdioMcpServer;
};
export const ensureMcpClient = async () => {
try {
if (mcpClient) {
const pingResult = await mcpClient.ping();
if (pingResult) {
return mcpClient;
}
}
const config = loadConfig();
mcpClient = new Client({ name: 'Mcp Chrome Proxy', version: '1.0.0' }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL(config.url), {});
await mcpClient.connect(transport);
return mcpClient;
} catch (error) {
mcpClient?.close();
mcpClient = null;
console.error('Failed to connect to MCP server:', error);
}
};
export const setupTools = (server: Server) => {
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments || {}),
);
// List resources handler - REQUIRED BY MCP PROTOCOL
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
// List prompts handler - REQUIRED BY MCP PROTOCOL
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
};
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
try {
const client = await ensureMcpClient();
if (!client) {
throw new Error('Failed to connect to MCP server');
}
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: 2 * 6 * 1000, // Default timeout of 2 minute
});
return result as CallToolResult;
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${error.message}`,
},
],
isError: true,
};
}
};
async function main() {
const transport = new StdioServerTransport();
await getStdioMcpServer().connect(transport);
}
main().catch((error) => {
console.error('Fatal error Chrome MCP Server main():', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/network-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
/**
* Network Capture Helper
*
* This script helps replay network requests with the original cookies and headers.
*/
// Prevent duplicate initialization
if (window.__NETWORK_CAPTURE_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__NETWORK_CAPTURE_HELPER_INITIALIZED__ = true;
/**
* Replay a network request
* @param {string} url - The URL to send the request to
* @param {string} method - The HTTP method to use
* @param {Object} headers - The headers to include in the request
* @param {any} body - The body of the request
* @param {number} timeout - Timeout in milliseconds (default: 30000)
* @returns {Promise<Object>} - The response data
*/
async function replayNetworkRequest(url, method, headers, body, timeout = 30000) {
try {
// Create fetch options
const options = {
method: method,
headers: headers || {},
credentials: 'include', // Include cookies
mode: 'cors',
cache: 'no-cache',
};
// Add body for non-GET requests
if (method !== 'GET' && method !== 'HEAD' && body !== undefined) {
options.body = body;
}
// 创建一个带超时的 fetch
const fetchWithTimeout = async (url, options, timeout) => {
const controller = new AbortController();
const signal = controller.signal;
// 设置超时
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { ...options, signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
};
// 发送带超时的请求
const response = await fetchWithTimeout(url, options, timeout);
// Process response
const responseData = {
status: response.status,
statusText: response.statusText,
headers: {},
};
// Get response headers
response.headers.forEach((value, key) => {
responseData.headers[key] = value;
});
// Try to get response body based on content type
const contentType = response.headers.get('content-type') || '';
try {
if (contentType.includes('application/json')) {
responseData.body = await response.json();
} else if (
contentType.includes('text/') ||
contentType.includes('application/xml') ||
contentType.includes('application/javascript')
) {
responseData.body = await response.text();
} else {
// For binary data, just indicate it was received but not parsed
responseData.body = '[Binary data not displayed]';
}
} catch (error) {
responseData.body = `[Error parsing response body: ${error.message}]`;
}
return {
success: true,
response: responseData,
};
} catch (error) {
console.error('Error replaying request:', error);
return {
success: false,
error: `Error replaying request: ${error.message}`,
};
}
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
// Respond to ping message
if (request.action === 'chrome_network_request_ping') {
sendResponse({ status: 'pong' });
return false; // Synchronous response
} else if (request.action === 'sendPureNetworkRequest') {
replayNetworkRequest(
request.url,
request.method,
request.headers,
request.body,
request.timeout,
)
.then(sendResponse)
.catch((error) => {
sendResponse({
success: false,
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
}
});
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/storage-manager.ts:
--------------------------------------------------------------------------------
```typescript
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
/**
* Get storage statistics
*/
export async function handleGetStorageStats(): Promise<{
success: boolean;
stats?: any;
error?: string;
}> {
try {
// Get ContentIndexer statistics
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
// Note: Semantic engine initialization is now user-controlled
// ContentIndexer will be initialized when user manually triggers semantic engine initialization
// Get statistics
const stats = contentIndexer.getStats();
return {
success: true,
stats: {
indexedPages: stats.indexedPages || 0,
totalDocuments: stats.totalDocuments || 0,
totalTabs: stats.totalTabs || 0,
indexSize: stats.indexSize || 0,
isInitialized: stats.isInitialized || false,
semanticEngineReady: stats.semanticEngineReady || false,
semanticEngineInitializing: stats.semanticEngineInitializing || false,
},
};
} catch (error: any) {
console.error('Background: Failed to get storage stats:', error);
return {
success: false,
error: error.message,
stats: {
indexedPages: 0,
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
isInitialized: false,
semanticEngineReady: false,
semanticEngineInitializing: false,
},
};
}
}
/**
* Clear all data
*/
export async function handleClearAllData(): Promise<{ success: boolean; error?: string }> {
try {
// 1. Clear all ContentIndexer indexes
try {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
await contentIndexer.clearAllIndexes();
console.log('Storage: ContentIndexer indexes cleared successfully');
} catch (indexerError) {
console.warn('Background: Failed to clear ContentIndexer indexes:', indexerError);
// Continue with other cleanup operations
}
// 2. Clear all VectorDatabase data
try {
const { clearAllVectorData } = await import('@/utils/vector-database');
await clearAllVectorData();
console.log('Storage: Vector database data cleared successfully');
} catch (vectorError) {
console.warn('Background: Failed to clear vector data:', vectorError);
// Continue with other cleanup operations
}
// 3. Clear related data in chrome.storage (preserve model preferences)
try {
const keysToRemove = ['vectorDatabaseStats', 'lastCleanupTime', 'contentIndexerStats'];
await chrome.storage.local.remove(keysToRemove);
console.log('Storage: Chrome storage data cleared successfully');
} catch (storageError) {
console.warn('Background: Failed to clear chrome storage data:', storageError);
}
return { success: true };
} catch (error: any) {
console.error('Background: Failed to clear all data:', error);
return { success: false, error: error.message };
}
}
/**
* Initialize storage manager module message listeners
*/
export const initStorageManagerListener = () => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === BACKGROUND_MESSAGE_TYPES.GET_STORAGE_STATS) {
handleGetStorageStats()
.then((result: { success: boolean; stats?: any; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.CLEAR_ALL_DATA) {
handleClearAllData()
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
}
});
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/message-types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Consolidated message type constants for Chrome extension communication
* Note: Native message types are imported from the shared package
*/
// Message targets for routing
export enum MessageTarget {
Offscreen = 'offscreen',
ContentScript = 'content_script',
Background = 'background',
}
// Background script message types
export const BACKGROUND_MESSAGE_TYPES = {
SWITCH_SEMANTIC_MODEL: 'switch_semantic_model',
GET_MODEL_STATUS: 'get_model_status',
UPDATE_MODEL_STATUS: 'update_model_status',
GET_STORAGE_STATS: 'get_storage_stats',
CLEAR_ALL_DATA: 'clear_all_data',
GET_SERVER_STATUS: 'get_server_status',
REFRESH_SERVER_STATUS: 'refresh_server_status',
SERVER_STATUS_CHANGED: 'server_status_changed',
INITIALIZE_SEMANTIC_ENGINE: 'initialize_semantic_engine',
} as const;
// Offscreen message types
export const OFFSCREEN_MESSAGE_TYPES = {
SIMILARITY_ENGINE_INIT: 'similarityEngineInit',
SIMILARITY_ENGINE_COMPUTE: 'similarityEngineCompute',
SIMILARITY_ENGINE_BATCH_COMPUTE: 'similarityEngineBatchCompute',
SIMILARITY_ENGINE_STATUS: 'similarityEngineStatus',
} as const;
// Content script message types
export const CONTENT_MESSAGE_TYPES = {
WEB_FETCHER_GET_TEXT_CONTENT: 'webFetcherGetTextContent',
WEB_FETCHER_GET_HTML_CONTENT: 'getHtmlContent',
NETWORK_CAPTURE_PING: 'network_capture_ping',
CLICK_HELPER_PING: 'click_helper_ping',
FILL_HELPER_PING: 'fill_helper_ping',
KEYBOARD_HELPER_PING: 'keyboard_helper_ping',
SCREENSHOT_HELPER_PING: 'screenshot_helper_ping',
INTERACTIVE_ELEMENTS_HELPER_PING: 'interactive_elements_helper_ping',
} as const;
// Tool action message types (for chrome.runtime.sendMessage)
export const TOOL_MESSAGE_TYPES = {
// Screenshot related
SCREENSHOT_PREPARE_PAGE_FOR_CAPTURE: 'preparePageForCapture',
SCREENSHOT_GET_PAGE_DETAILS: 'getPageDetails',
SCREENSHOT_GET_ELEMENT_DETAILS: 'getElementDetails',
SCREENSHOT_SCROLL_PAGE: 'scrollPage',
SCREENSHOT_RESET_PAGE_AFTER_CAPTURE: 'resetPageAfterCapture',
// Web content fetching
WEB_FETCHER_GET_HTML_CONTENT: 'getHtmlContent',
WEB_FETCHER_GET_TEXT_CONTENT: 'getTextContent',
// User interactions
CLICK_ELEMENT: 'clickElement',
FILL_ELEMENT: 'fillElement',
SIMULATE_KEYBOARD: 'simulateKeyboard',
// Interactive elements
GET_INTERACTIVE_ELEMENTS: 'getInteractiveElements',
// Network requests
NETWORK_SEND_REQUEST: 'sendPureNetworkRequest',
// Semantic similarity engine
SIMILARITY_ENGINE_INIT: 'similarityEngineInit',
SIMILARITY_ENGINE_COMPUTE_BATCH: 'similarityEngineComputeBatch',
} as const;
// Type unions for type safety
export type BackgroundMessageType =
(typeof BACKGROUND_MESSAGE_TYPES)[keyof typeof BACKGROUND_MESSAGE_TYPES];
export type OffscreenMessageType =
(typeof OFFSCREEN_MESSAGE_TYPES)[keyof typeof OFFSCREEN_MESSAGE_TYPES];
export type ContentMessageType = (typeof CONTENT_MESSAGE_TYPES)[keyof typeof CONTENT_MESSAGE_TYPES];
export type ToolMessageType = (typeof TOOL_MESSAGE_TYPES)[keyof typeof TOOL_MESSAGE_TYPES];
// Legacy enum for backward compatibility (will be deprecated)
export enum SendMessageType {
// Screenshot related message types
ScreenshotPreparePageForCapture = 'preparePageForCapture',
ScreenshotGetPageDetails = 'getPageDetails',
ScreenshotGetElementDetails = 'getElementDetails',
ScreenshotScrollPage = 'scrollPage',
ScreenshotResetPageAfterCapture = 'resetPageAfterCapture',
// Web content fetching related message types
WebFetcherGetHtmlContent = 'getHtmlContent',
WebFetcherGetTextContent = 'getTextContent',
// Click related message types
ClickElement = 'clickElement',
// Input filling related message types
FillElement = 'fillElement',
// Interactive elements related message types
GetInteractiveElements = 'getInteractiveElements',
// Network request capture related message types
NetworkSendRequest = 'sendPureNetworkRequest',
// Keyboard event related message types
SimulateKeyboard = 'simulateKeyboard',
// Semantic similarity engine related message types
SimilarityEngineInit = 'similarityEngineInit',
SimilarityEngineComputeBatch = 'similarityEngineComputeBatch',
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/style.css:
--------------------------------------------------------------------------------
```css
/* 现代化全局样式 */
:root {
/* 字体系统 */
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
font-weight: 400;
/* 颜色系统 */
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--primary-color: #667eea;
--primary-dark: #5a67d8;
--secondary-color: #764ba2;
--success-color: #48bb78;
--warning-color: #ed8936;
--error-color: #f56565;
--info-color: #4299e1;
--text-primary: #2d3748;
--text-secondary: #4a5568;
--text-muted: #718096;
--text-light: #a0aec0;
--bg-primary: #ffffff;
--bg-secondary: #f7fafc;
--bg-tertiary: #edf2f7;
--bg-overlay: rgba(255, 255, 255, 0.95);
--border-color: #e2e8f0;
--border-light: #f1f5f9;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
/* 间距系统 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 20px;
--spacing-2xl: 24px;
--spacing-3xl: 32px;
/* 圆角系统 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
/* 动画 */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
/* 字体渲染优化 */
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
/* 重置样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0;
width: 400px;
min-height: 500px;
max-height: 600px;
overflow: hidden;
font-family: inherit;
background: var(--bg-secondary);
color: var(--text-primary);
}
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
/* 链接样式 */
a {
color: var(--primary-color);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--primary-dark);
}
/* 按钮基础样式重置 */
button {
font-family: inherit;
font-size: inherit;
line-height: inherit;
border: none;
background: none;
cursor: pointer;
transition: all var(--transition-normal);
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* 输入框基础样式 */
input,
textarea,
select {
font-family: inherit;
font-size: inherit;
line-height: inherit;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-primary);
color: var(--text-primary);
transition: all var(--transition-fast);
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: var(--radius-sm);
transition: background var(--transition-fast);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* 选择文本样式 */
::selection {
background: rgba(102, 126, 234, 0.2);
color: var(--text-primary);
}
/* 焦点可见性 */
:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 动画关键帧 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式断点 */
@media (max-width: 420px) {
:root {
--spacing-xs: 3px;
--spacing-sm: 6px;
--spacing-md: 10px;
--spacing-lg: 14px;
--spacing-xl: 18px;
--spacing-2xl: 22px;
--spacing-3xl: 28px;
}
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
:root {
--border-color: #000000;
--text-muted: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/run_host.sh:
--------------------------------------------------------------------------------
```bash
#!/usr/bin/env bash
# Configuration
ENABLE_LOG_ROTATION="true"
LOG_RETENTION_COUNT=5
# Setup paths
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="${SCRIPT_DIR}/logs"
mkdir -p "${LOG_DIR}"
# Log rotation
if [ "${ENABLE_LOG_ROTATION}" = "true" ]; then
ls -tp "${LOG_DIR}/native_host_wrapper_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
ls -tp "${LOG_DIR}/native_host_stderr_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
fi
# Logging setup
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
WRAPPER_LOG="${LOG_DIR}/native_host_wrapper_macos_${TIMESTAMP}.log"
STDERR_LOG="${LOG_DIR}/native_host_stderr_macos_${TIMESTAMP}.log"
NODE_SCRIPT="${SCRIPT_DIR}/index.js"
# Initial logging
{
echo "--- Wrapper script called at $(date) ---"
echo "SCRIPT_DIR: ${SCRIPT_DIR}"
echo "LOG_DIR: ${LOG_DIR}"
echo "NODE_SCRIPT: ${NODE_SCRIPT}"
echo "Initial PATH: ${PATH}"
echo "User: $(whoami)"
echo "Current PWD: $(pwd)"
} > "${WRAPPER_LOG}"
# Node.js discovery
NODE_EXEC=""
# Priority 1: Installation-time node path
NODE_PATH_FILE="${SCRIPT_DIR}/node_path.txt"
echo "Searching for Node.js..." >> "${WRAPPER_LOG}"
echo "[Priority 1] Checking installation-time node path" >> "${WRAPPER_LOG}"
if [ -f "${NODE_PATH_FILE}" ]; then
EXPECTED_NODE=$(cat "${NODE_PATH_FILE}" 2>/dev/null | tr -d '\n\r')
if [ -n "${EXPECTED_NODE}" ] && [ -x "${EXPECTED_NODE}" ]; then
NODE_EXEC="${EXPECTED_NODE}"
echo "Found installation-time node at ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 1.5: Fallback to relative path
if [ -z "${NODE_EXEC}" ]; then
EXPECTED_NODE="${SCRIPT_DIR}/../../../bin/node"
echo "[Priority 1.5] Checking relative path" >> "${WRAPPER_LOG}"
if [ -x "${EXPECTED_NODE}" ]; then
NODE_EXEC="${EXPECTED_NODE}"
echo "Found node at relative path: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 2: NVM
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 2] Checking NVM" >> "${WRAPPER_LOG}"
NVM_DIR="$HOME/.nvm"
if [ -d "${NVM_DIR}" ]; then
# Try default version first
if [ -L "${NVM_DIR}/alias/default" ]; then
NVM_DEFAULT_VERSION=$(readlink "${NVM_DIR}/alias/default")
NVM_DEFAULT_NODE="${NVM_DIR}/versions/node/${NVM_DEFAULT_VERSION}/bin/node"
if [ -x "${NVM_DEFAULT_NODE}" ]; then
NODE_EXEC="${NVM_DEFAULT_NODE}"
echo "Found NVM default node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Fallback to latest version
if [ -z "${NODE_EXEC}" ]; then
LATEST_NVM_VERSION_PATH=$(ls -d ${NVM_DIR}/versions/node/v* 2>/dev/null | sort -V | tail -n 1)
if [ -n "${LATEST_NVM_VERSION_PATH}" ] && [ -x "${LATEST_NVM_VERSION_PATH}/bin/node" ]; then
NODE_EXEC="${LATEST_NVM_VERSION_PATH}/bin/node"
echo "Found NVM latest node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
fi
fi
# Priority 3: Common paths
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 3] Checking common paths" >> "${WRAPPER_LOG}"
COMMON_NODE_PATHS=(
"/opt/homebrew/bin/node"
"/usr/local/bin/node"
)
for path_to_node in "${COMMON_NODE_PATHS[@]}"; do
if [ -x "${path_to_node}" ]; then
NODE_EXEC="${path_to_node}"
echo "Found node at: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
break
fi
done
fi
# Priority 4: command -v
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 4] Trying 'command -v node'" >> "${WRAPPER_LOG}"
if command -v node &>/dev/null; then
NODE_EXEC=$(command -v node)
echo "Found node using 'command -v': ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 5: PATH search
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 5] Searching PATH" >> "${WRAPPER_LOG}"
OLD_IFS=$IFS
IFS=:
for path_in_env in $PATH; do
if [ -x "${path_in_env}/node" ]; then
NODE_EXEC="${path_in_env}/node"
echo "Found node in PATH: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
break
fi
done
IFS=$OLD_IFS
fi
# Execution
if [ -z "${NODE_EXEC}" ]; then
{
echo "ERROR: Node.js executable not found!"
echo "Searched: installation path, relative path, NVM, common paths, command -v, PATH"
} >> "${WRAPPER_LOG}"
exit 1
fi
{
echo "Using Node executable: ${NODE_EXEC}"
echo "Node version: $(${NODE_EXEC} -v)"
echo "Executing: ${NODE_EXEC} ${NODE_SCRIPT}"
} >> "${WRAPPER_LOG}"
exec "${NODE_EXEC}" "${NODE_SCRIPT}" 2>> "${STDERR_LOG}"
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/interaction.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants';
interface Coordinates {
x: number;
y: number;
}
interface ClickToolParams {
selector?: string; // CSS selector for the element to click
coordinates?: Coordinates; // Coordinates to click at (x, y relative to viewport)
waitForNavigation?: boolean; // Whether to wait for navigation to complete after click
timeout?: number; // Timeout in milliseconds for waiting for the element or navigation
}
/**
* Tool for clicking elements on web pages
*/
class ClickTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.CLICK;
/**
* Execute click operation
*/
async execute(args: ClickToolParams): Promise<ToolResult> {
const {
selector,
coordinates,
waitForNavigation = false,
timeout = TIMEOUTS.DEFAULT_WAIT * 5,
} = args;
console.log(`Starting click operation with options:`, args);
if (!selector && !coordinates) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Either selector or coordinates must be provided',
);
}
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/click-helper.js']);
// Send click message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.CLICK_ELEMENT,
selector,
coordinates,
waitForNavigation,
timeout,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Click operation successful',
elementInfo: result.elementInfo,
navigationOccurred: result.navigationOccurred,
clickMethod: coordinates ? 'coordinates' : 'selector',
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in click operation:', error);
return createErrorResponse(
`Error performing click: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const clickTool = new ClickTool();
interface FillToolParams {
selector: string;
value: string;
}
/**
* Tool for filling form elements on web pages
*/
class FillTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.FILL;
/**
* Execute fill operation
*/
async execute(args: FillToolParams): Promise<ToolResult> {
const { selector, value } = args;
console.log(`Starting fill operation with options:`, args);
if (!selector) {
return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Selector must be provided');
}
if (value === undefined || value === null) {
return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Value must be provided');
}
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND);
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID');
}
await this.injectContentScript(tab.id, ['inject-scripts/fill-helper.js']);
// Send fill message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.FILL_ELEMENT,
selector,
value,
});
if (result.error) {
return createErrorResponse(result.error);
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: result.message || 'Fill operation successful',
elementInfo: result.elementInfo,
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in fill operation:', error);
return createErrorResponse(
`Error filling element: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const fillTool = new FillTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/screenshot-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
/**
* Screenshot helper content script
* Handles page preparation, scrolling, element positioning, etc.
*/
if (window.__SCREENSHOT_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__SCREENSHOT_HELPER_INITIALIZED__ = true;
// Save original styles
let originalOverflowStyle = '';
let hiddenFixedElements = [];
/**
* Get fixed/sticky positioned elements
* @returns Array of fixed/sticky elements
*/
function getFixedElements() {
const fixed = [];
document.querySelectorAll('*').forEach((el) => {
const htmlEl = el;
const style = window.getComputedStyle(htmlEl);
if (style.position === 'fixed' || style.position === 'sticky') {
// Filter out tiny or invisible elements, and elements that are part of the extension UI
if (
htmlEl.offsetWidth > 1 &&
htmlEl.offsetHeight > 1 &&
!htmlEl.id.startsWith('chrome-mcp-')
) {
fixed.push({
element: htmlEl,
originalDisplay: htmlEl.style.display,
originalVisibility: htmlEl.style.visibility,
});
}
}
});
return fixed;
}
/**
* Hide fixed/sticky elements
*/
function hideFixedElements() {
hiddenFixedElements = getFixedElements();
hiddenFixedElements.forEach((item) => {
item.element.style.display = 'none';
});
}
/**
* Restore fixed/sticky elements
*/
function showFixedElements() {
hiddenFixedElements.forEach((item) => {
item.element.style.display = item.originalDisplay || '';
});
hiddenFixedElements = [];
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
// Respond to ping message
if (request.action === 'chrome_screenshot_ping') {
sendResponse({ status: 'pong' });
return false; // Synchronous response
}
// Prepare page for capture
else if (request.action === 'preparePageForCapture') {
originalOverflowStyle = document.documentElement.style.overflow;
document.documentElement.style.overflow = 'hidden'; // Hide main scrollbar
if (request.options?.fullPage) {
// Only hide fixed elements for full page to avoid flicker
hideFixedElements();
}
// Give styles a moment to apply
setTimeout(() => {
sendResponse({ success: true });
}, 50);
return true; // Async response
}
// Get page details
else if (request.action === 'getPageDetails') {
const body = document.body;
const html = document.documentElement;
sendResponse({
totalWidth: Math.max(
body.scrollWidth,
body.offsetWidth,
html.clientWidth,
html.scrollWidth,
html.offsetWidth,
),
totalHeight: Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
),
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio || 1,
currentScrollX: window.scrollX,
currentScrollY: window.scrollY,
});
}
// Get element details
else if (request.action === 'getElementDetails') {
const element = document.querySelector(request.selector);
if (element) {
element.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
setTimeout(() => {
// Wait for scroll
const rect = element.getBoundingClientRect();
sendResponse({
rect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
devicePixelRatio: window.devicePixelRatio || 1,
});
}, 200); // Increased delay for scrollIntoView
return true; // Async response
} else {
sendResponse({ error: `Element with selector "${request.selector}" not found.` });
}
return true; // Async response
}
// Scroll page
else if (request.action === 'scrollPage') {
window.scrollTo({ left: request.x, top: request.y, behavior: 'instant' });
// Wait for scroll and potential reflows/lazy-loading
setTimeout(() => {
sendResponse({
success: true,
newScrollX: window.scrollX,
newScrollY: window.scrollY,
});
}, request.scrollDelay || 300); // Configurable delay
return true; // Async response
}
// Reset page
else if (request.action === 'resetPageAfterCapture') {
document.documentElement.style.overflow = originalOverflowStyle;
showFixedElements();
if (typeof request.scrollX !== 'undefined' && typeof request.scrollY !== 'undefined') {
window.scrollTo({ left: request.scrollX, top: request.scrollY, behavior: 'instant' });
}
sendResponse({ success: true });
}
return false; // Synchronous response
});
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ConfirmDialog.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div v-if="visible" class="confirmation-dialog" @click.self="$emit('cancel')">
<div class="dialog-content">
<div class="dialog-header">
<span class="dialog-icon">{{ icon }}</span>
<h3 class="dialog-title">{{ title }}</h3>
</div>
<div class="dialog-body">
<p class="dialog-message">{{ message }}</p>
<ul v-if="items && items.length > 0" class="dialog-list">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<div v-if="warning" class="dialog-warning">
<strong>{{ warning }}</strong>
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button cancel-button" @click="$emit('cancel')">
{{ cancelText }}
</button>
<button
class="dialog-button confirm-button"
:disabled="isConfirming"
@click="$emit('confirm')"
>
{{ isConfirming ? confirmingText : confirmText }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getMessage } from '@/utils/i18n';
interface Props {
visible: boolean;
title: string;
message: string;
items?: string[];
warning?: string;
icon?: string;
confirmText?: string;
cancelText?: string;
confirmingText?: string;
isConfirming?: boolean;
}
interface Emits {
(e: 'confirm'): void;
(e: 'cancel'): void;
}
withDefaults(defineProps<Props>(), {
icon: '⚠️',
confirmText: getMessage('confirmButton'),
cancelText: getMessage('cancelButton'),
confirmingText: getMessage('processingStatus'),
isConfirming: false,
});
defineEmits<Emits>();
</script>
<style scoped>
.confirmation-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(8px);
animation: dialogFadeIn 0.3s ease-out;
}
@keyframes dialogFadeIn {
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(8px);
}
}
.dialog-content {
background: white;
border-radius: 12px;
padding: 24px;
max-width: 360px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: dialogSlideIn 0.3s ease-out;
border: 1px solid rgba(255, 255, 255, 0.2);
}
@keyframes dialogSlideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dialog-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.dialog-icon {
font-size: 24px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.dialog-body {
margin-bottom: 24px;
}
.dialog-message {
font-size: 14px;
color: #4a5568;
margin: 0 0 16px 0;
line-height: 1.6;
}
.dialog-list {
margin: 16px 0;
padding-left: 24px;
background: linear-gradient(135deg, #f7fafc, #edf2f7);
border-radius: 6px;
padding: 12px 12px 12px 32px;
border-left: 3px solid #667eea;
}
.dialog-list li {
font-size: 13px;
color: #718096;
margin-bottom: 6px;
line-height: 1.4;
}
.dialog-list li:last-child {
margin-bottom: 0;
}
.dialog-warning {
font-size: 13px;
color: #e53e3e;
margin: 16px 0 0 0;
padding: 12px;
background: linear-gradient(135deg, rgba(245, 101, 101, 0.1), rgba(229, 62, 62, 0.05));
border-radius: 6px;
border-left: 3px solid #e53e3e;
border: 1px solid rgba(245, 101, 101, 0.2);
}
.dialog-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.dialog-button {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
}
.cancel-button {
background: linear-gradient(135deg, #e2e8f0, #cbd5e0);
color: #4a5568;
border: 1px solid #cbd5e0;
}
.cancel-button:hover {
background: linear-gradient(135deg, #cbd5e0, #a0aec0);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(160, 174, 192, 0.3);
}
.confirm-button {
background: linear-gradient(135deg, #f56565, #e53e3e);
color: white;
border: 1px solid #e53e3e;
}
.confirm-button:hover:not(:disabled) {
background: linear-gradient(135deg, #e53e3e, #c53030);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
}
.confirm-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 响应式设计 */
@media (max-width: 420px) {
.dialog-content {
padding: 20px;
max-width: 320px;
}
.dialog-header {
gap: 10px;
margin-bottom: 16px;
}
.dialog-icon {
font-size: 20px;
}
.dialog-title {
font-size: 16px;
}
.dialog-message {
font-size: 13px;
}
.dialog-list {
padding: 10px 10px 10px 28px;
}
.dialog-list li {
font-size: 12px;
}
.dialog-warning {
font-size: 12px;
padding: 10px;
}
.dialog-actions {
gap: 8px;
flex-direction: column-reverse;
}
.dialog-button {
width: 100%;
padding: 12px 16px;
}
}
/* 焦点样式 */
.dialog-button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
.cancel-button:focus {
box-shadow: 0 0 0 3px rgba(160, 174, 192, 0.3);
}
.confirm-button:focus {
box-shadow: 0 0 0 3px rgba(245, 101, 101, 0.3);
}
</style>
```
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.0.5]
### Improved
- **Image Compression**: Compress base64 images when using screenshot tool
- **Interactive Elements Detection Optimization**: Enhanced interactive elements detection tool with expanded search scope, now supports finding interactive div elements
## [v0.0.4]
### Added
- **STDIO Connection Support**: Added support for connecting to the MCP server via standard input/output (stdio) method
- **Console Output Capture Tool**: New `chrome_console` tool for capturing browser console output
## [v0.0.3]
### Added
- **Inject script tool**: For injecting content scripts into web page
- **Send command to inject script tool**: For sending commands to the injected script
## [v0.0.2]
### Added
- **Conditional Semantic Engine Initialization**: Smart cache-based initialization that only loads models when cached versions are available
- **Enhanced Model Cache Management**: Comprehensive cache management system with automatic cleanup and size limits
- **Windows Platform Compatibility**: Full support for Windows Chrome Native Messaging with registry-based manifest detection
- **Cache Statistics and Manual Management**: User interface for viewing cache stats and manual cache cleanup
- **Concurrent Initialization Protection**: Prevents duplicate initialization attempts across components
### Improved
- **Startup Performance**: Dramatically reduced startup time when no model cache exists (from ~3s to ~0.5s)
- **Memory Usage**: Optimized memory consumption through on-demand model loading
- **Cache Expiration Logic**: Intelligent cache expiration (14 days) with automatic cleanup
- **Error Handling**: Enhanced error handling for model initialization failures
- **Component Coordination**: Simplified initialization flow between semantic engine and content indexer
### Fixed
- **Windows Native Host Issues**: Resolved Node.js environment conflicts with multiple NVM installations
- **Race Condition Prevention**: Eliminated concurrent initialization attempts that could cause conflicts
- **Cache Size Management**: Automatic cleanup when cache exceeds 500MB limit
- **Model Download Optimization**: Prevents unnecessary model downloads during plugin startup
### Technical Improvements
- **ModelCacheManager**: Added `isModelCached()` and `hasAnyValidCache()` methods for cache detection
- **SemanticSimilarityEngine**: Added cache checking functions and conditional initialization logic
- **Background Script**: Implemented smart initialization based on cache availability
- **VectorSearchTool**: Simplified to passive initialization model
- **ContentIndexer**: Enhanced with semantic engine readiness checks
### Documentation
- Added comprehensive conditional initialization documentation
- Updated cache management system documentation
- Created troubleshooting guides for Windows platform issues
## [v0.0.1]
### Added
- **Core Browser Tools**: Complete set of browser automation tools for web interaction
- **Click Tool**: Intelligent element clicking with coordinate and selector support
- **Fill Tool**: Form filling with text input and selection capabilities
- **Screenshot Tool**: Full page and element-specific screenshot capture
- **Navigation Tools**: URL navigation and page interaction utilities
- **Keyboard Tool**: Keyboard input simulation and hotkey support
- **Vector Search Engine**: Advanced semantic search capabilities
- **Content Indexing**: Automatic indexing of browser tab content
- **Semantic Similarity**: AI-powered text similarity matching
- **Vector Database**: Efficient storage and retrieval of embeddings
- **Multi-language Support**: Comprehensive multilingual text processing
- **Native Host Integration**: Seamless communication with external applications
- **Chrome Native Messaging**: Bidirectional communication channel
- **Cross-platform Support**: Windows, macOS, and Linux compatibility
- **Message Protocol**: Structured messaging system for tool execution
- **AI Model Integration**: State-of-the-art language models for semantic processing
- **Transformer Models**: Support for multiple pre-trained models
- **ONNX Runtime**: Optimized model inference with WebAssembly
- **Model Management**: Dynamic model loading and switching
- **Performance Optimization**: SIMD acceleration and memory pooling
- **User Interface**: Intuitive popup interface for extension management
- **Model Selection**: Easy switching between different AI models
- **Status Monitoring**: Real-time initialization and download progress
- **Settings Management**: User preferences and configuration options
- **Cache Management**: Visual cache statistics and cleanup controls
### Technical Foundation
- **Extension Architecture**: Robust Chrome extension with background scripts and content injection
- **Worker-based Processing**: Offscreen document for heavy computational tasks
- **Memory Management**: LRU caching and efficient resource utilization
- **Error Handling**: Comprehensive error reporting and recovery mechanisms
- **TypeScript Implementation**: Full type safety and modern JavaScript features
### Initial Features
- Multi-tab content analysis and search
- Real-time semantic similarity computation
- Automated web page interaction
- Cross-platform native messaging
- Extensible tool framework for future enhancements
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/fill-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
// fill-helper.js
// This script is injected into the page to handle form filling operations
if (window.__FILL_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__FILL_HELPER_INITIALIZED__ = true;
/**
* Fill an input element with the specified value
* @param {string} selector - CSS selector for the element to fill
* @param {string} value - Value to fill into the element
* @returns {Promise<Object>} - Result of the fill operation
*/
async function fillElement(selector, value) {
try {
// Find the element
const element = document.querySelector(selector);
if (!element) {
return {
error: `Element with selector "${selector}" not found`,
};
}
// Get element information
const rect = element.getBoundingClientRect();
const elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
type: element.type || null,
isVisible: isElementVisible(element),
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
};
// Check if element is visible
if (!elementInfo.isVisible) {
return {
error: `Element with selector "${selector}" is not visible`,
elementInfo,
};
}
// Check if element is an input, textarea, or select
const validTags = ['INPUT', 'TEXTAREA', 'SELECT'];
const validInputTypes = [
'text',
'email',
'password',
'number',
'search',
'tel',
'url',
'date',
'datetime-local',
'month',
'time',
'week',
'color',
];
if (!validTags.includes(element.tagName)) {
return {
error: `Element with selector "${selector}" is not a fillable element (must be INPUT, TEXTAREA, or SELECT)`,
elementInfo,
};
}
// For input elements, check if the type is valid
if (
element.tagName === 'INPUT' &&
!validInputTypes.includes(element.type) &&
element.type !== null
) {
return {
error: `Input element with selector "${selector}" has type "${element.type}" which is not fillable`,
elementInfo,
};
}
// Scroll element into view
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
await new Promise((resolve) => setTimeout(resolve, 100));
// Focus the element
element.focus();
// Fill the element based on its type
if (element.tagName === 'SELECT') {
// For select elements, find the option with matching value or text
let optionFound = false;
for (const option of element.options) {
if (option.value === value || option.text === value) {
element.value = option.value;
optionFound = true;
break;
}
}
if (!optionFound) {
return {
error: `No option with value or text "${value}" found in select element`,
elementInfo,
};
}
// Trigger change event
element.dispatchEvent(new Event('change', { bubbles: true }));
} else {
// For input and textarea elements
// Clear the current value
element.value = '';
element.dispatchEvent(new Event('input', { bubbles: true }));
// Set the new value
element.value = value;
// Trigger input and change events
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
// Blur the element
element.blur();
return {
success: true,
message: 'Element filled successfully',
elementInfo: {
...elementInfo,
value: element.value, // Include the final value in the response
},
};
} catch (error) {
return {
error: `Error filling element: ${error.message}`,
};
}
}
/**
* Check if an element is visible
* @param {Element} element - The element to check
* @returns {boolean} - Whether the element is visible
*/
function isElementVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
// Check if element is within viewport
if (
rect.bottom < 0 ||
rect.top > window.innerHeight ||
rect.right < 0 ||
rect.left > window.innerWidth
) {
return false;
}
// Check if element is actually visible at its center point
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
if (!elementAtPoint) return false;
return element === elementAtPoint || element.contains(elementAtPoint);
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'fillElement') {
fillElement(request.selector, request.value)
.then(sendResponse)
.catch((error) => {
sendResponse({
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'chrome_fill_or_select_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}
```
--------------------------------------------------------------------------------
/app/native-server/src/file-handler.ts:
--------------------------------------------------------------------------------
```typescript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
import fetch from 'node-fetch';
/**
* File handler for managing file uploads through the native messaging host
*/
export class FileHandler {
private tempDir: string;
constructor() {
// Create a temp directory for file operations
this.tempDir = path.join(os.tmpdir(), 'chrome-mcp-uploads');
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true });
}
}
/**
* Handle file preparation request from the extension
*/
async handleFileRequest(request: any): Promise<any> {
const { action, fileUrl, base64Data, fileName, filePath } = request;
try {
switch (action) {
case 'prepareFile':
if (fileUrl) {
return await this.downloadFile(fileUrl, fileName);
} else if (base64Data) {
return await this.saveBase64File(base64Data, fileName);
} else if (filePath) {
return await this.verifyFile(filePath);
}
break;
case 'cleanupFile':
return await this.cleanupFile(filePath);
default:
return {
success: false,
error: `Unknown file action: ${action}`,
};
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Download a file from URL and save to temp directory
*/
private async downloadFile(fileUrl: string, fileName?: string): Promise<any> {
try {
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to download file: ${response.statusText}`);
}
// Generate filename if not provided
const finalFileName = fileName || this.generateFileName(fileUrl);
const filePath = path.join(this.tempDir, finalFileName);
// Get the file buffer
const buffer = await response.buffer();
// Save to file
fs.writeFileSync(filePath, buffer);
return {
success: true,
filePath: filePath,
fileName: finalFileName,
size: buffer.length,
};
} catch (error) {
throw new Error(`Failed to download file from URL: ${error}`);
}
}
/**
* Save base64 data as a file
*/
private async saveBase64File(base64Data: string, fileName?: string): Promise<any> {
try {
// Remove data URL prefix if present
const base64Content = base64Data.replace(/^data:.*?;base64,/, '');
// Convert base64 to buffer
const buffer = Buffer.from(base64Content, 'base64');
// Generate filename if not provided
const finalFileName = fileName || `upload-${Date.now()}.bin`;
const filePath = path.join(this.tempDir, finalFileName);
// Save to file
fs.writeFileSync(filePath, buffer);
return {
success: true,
filePath: filePath,
fileName: finalFileName,
size: buffer.length,
};
} catch (error) {
throw new Error(`Failed to save base64 file: ${error}`);
}
}
/**
* Verify that a file exists and is accessible
*/
private async verifyFile(filePath: string): Promise<any> {
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`File does not exist: ${filePath}`);
}
// Get file stats
const stats = fs.statSync(filePath);
// Check if it's actually a file
if (!stats.isFile()) {
throw new Error(`Path is not a file: ${filePath}`);
}
// Check if file is readable
fs.accessSync(filePath, fs.constants.R_OK);
return {
success: true,
filePath: filePath,
fileName: path.basename(filePath),
size: stats.size,
};
} catch (error) {
throw new Error(`Failed to verify file: ${error}`);
}
}
/**
* Clean up a temporary file
*/
private async cleanupFile(filePath: string): Promise<any> {
try {
// Only allow cleanup of files in our temp directory
if (!filePath.startsWith(this.tempDir)) {
return {
success: false,
error: 'Can only cleanup files in temp directory',
};
}
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
return {
success: true,
message: 'File cleaned up successfully',
};
} catch (error) {
return {
success: false,
error: `Failed to cleanup file: ${error}`,
};
}
}
/**
* Generate a filename from URL or create a unique one
*/
private generateFileName(url?: string): string {
if (url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const basename = path.basename(pathname);
if (basename && basename !== '/') {
// Add random suffix to avoid collisions
const ext = path.extname(basename);
const name = path.basename(basename, ext);
const randomSuffix = crypto.randomBytes(4).toString('hex');
return `${name}-${randomSuffix}${ext}`;
}
} catch {
// Invalid URL, fall through to generate random name
}
}
// Generate random filename
return `upload-${crypto.randomBytes(8).toString('hex')}.bin`;
}
/**
* Clean up old temporary files (older than 1 hour)
*/
cleanupOldFiles(): void {
try {
const now = Date.now();
const oneHour = 60 * 60 * 1000;
const files = fs.readdirSync(this.tempDir);
for (const file of files) {
const filePath = path.join(this.tempDir, file);
const stats = fs.statSync(filePath);
if (now - stats.mtimeMs > oneHour) {
fs.unlinkSync(filePath);
console.log(`Cleaned up old temp file: ${file}`);
}
}
} catch (error) {
console.error('Error cleaning up old files:', error);
}
}
}
export default new FileHandler();
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/image-utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Image processing utility functions
*/
/**
* Create ImageBitmap from data URL (for OffscreenCanvas)
* @param dataUrl Image data URL
* @returns Created ImageBitmap object
*/
export async function createImageBitmapFromUrl(dataUrl: string): Promise<ImageBitmap> {
const response = await fetch(dataUrl);
const blob = await response.blob();
return await createImageBitmap(blob);
}
/**
* Stitch multiple image parts (dataURL) onto a single canvas
* @param parts Array of image parts, each containing dataUrl and y coordinate
* @param totalWidthPx Total width (pixels)
* @param totalHeightPx Total height (pixels)
* @returns Stitched canvas
*/
export async function stitchImages(
parts: { dataUrl: string; y: number }[],
totalWidthPx: number,
totalHeightPx: number,
): Promise<OffscreenCanvas> {
const canvas = new OffscreenCanvas(totalWidthPx, totalHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const part of parts) {
try {
const img = await createImageBitmapFromUrl(part.dataUrl);
const sx = 0;
const sy = 0;
const sWidth = img.width;
let sHeight = img.height;
const dy = part.y;
if (dy + sHeight > totalHeightPx) {
sHeight = totalHeightPx - dy;
}
if (sHeight <= 0) continue;
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, dy, sWidth, sHeight);
} catch (error) {
console.error('Error stitching image part:', error, part);
}
}
return canvas;
}
/**
* Crop image (from dataURL) to specified rectangle and resize
* @param originalDataUrl Original image data URL
* @param cropRectPx Crop rectangle (physical pixels)
* @param dpr Device pixel ratio
* @param targetWidthOpt Optional target output width (CSS pixels)
* @param targetHeightOpt Optional target output height (CSS pixels)
* @returns Cropped canvas
*/
export async function cropAndResizeImage(
originalDataUrl: string,
cropRectPx: { x: number; y: number; width: number; height: number },
dpr: number = 1,
targetWidthOpt?: number,
targetHeightOpt?: number,
): Promise<OffscreenCanvas> {
const img = await createImageBitmapFromUrl(originalDataUrl);
let sx = cropRectPx.x;
let sy = cropRectPx.y;
let sWidth = cropRectPx.width;
let sHeight = cropRectPx.height;
// Ensure crop area is within image boundaries
if (sx < 0) {
sWidth += sx;
sx = 0;
}
if (sy < 0) {
sHeight += sy;
sy = 0;
}
if (sx + sWidth > img.width) {
sWidth = img.width - sx;
}
if (sy + sHeight > img.height) {
sHeight = img.height - sy;
}
if (sWidth <= 0 || sHeight <= 0) {
throw new Error(
'Invalid calculated crop size (<=0). Element may not be visible or fully captured.',
);
}
const finalCanvasWidthPx = targetWidthOpt ? targetWidthOpt * dpr : sWidth;
const finalCanvasHeightPx = targetHeightOpt ? targetHeightOpt * dpr : sHeight;
const canvas = new OffscreenCanvas(finalCanvasWidthPx, finalCanvasHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, finalCanvasWidthPx, finalCanvasHeightPx);
return canvas;
}
/**
* Convert canvas to data URL
* @param canvas Canvas
* @param format Image format
* @param quality JPEG quality (0-1)
* @returns Data URL
*/
export async function canvasToDataURL(
canvas: OffscreenCanvas,
format: string = 'image/png',
quality?: number,
): Promise<string> {
const blob = await canvas.convertToBlob({
type: format,
quality: format === 'image/jpeg' ? quality : undefined,
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Compresses an image by scaling it and converting it to a target format with a specific quality.
* This is the most effective way to reduce image data size for transport or storage.
*
* @param {string} imageDataUrl - The original image data URL (e.g., from captureVisibleTab).
* @param {object} options - Compression options.
* @param {number} [options.scale=1.0] - The scaling factor for dimensions (e.g., 0.7 for 70%).
* @param {number} [options.quality=0.8] - The quality for lossy formats like JPEG (0.0 to 1.0).
* @param {string} [options.format='image/jpeg'] - The target image format.
* @returns {Promise<{dataUrl: string, mimeType: string}>} A promise that resolves to the compressed image data URL and its MIME type.
*/
export async function compressImage(
imageDataUrl: string,
options: { scale?: number; quality?: number; format?: 'image/jpeg' | 'image/webp' },
): Promise<{ dataUrl: string; mimeType: string }> {
const { scale = 1.0, quality = 0.8, format = 'image/jpeg' } = options;
// 1. Create an ImageBitmap from the original data URL for efficient drawing.
const imageBitmap = await createImageBitmapFromUrl(imageDataUrl);
// 2. Calculate the new dimensions based on the scale factor.
const newWidth = Math.round(imageBitmap.width * scale);
const newHeight = Math.round(imageBitmap.height * scale);
// 3. Use OffscreenCanvas for performance, as it doesn't need to be in the DOM.
const canvas = new OffscreenCanvas(newWidth, newHeight);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2D context from OffscreenCanvas');
}
// 4. Draw the original image onto the smaller canvas, effectively resizing it.
ctx.drawImage(imageBitmap, 0, 0, newWidth, newHeight);
// 5. Export the canvas content to the target format with the specified quality.
// This is the step that performs the data compression.
const compressedDataUrl = await canvas.convertToBlob({ type: format, quality: quality });
// A helper to convert blob to data URL since OffscreenCanvas.toDataURL is not standard yet
// on all execution contexts (like service workers).
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(compressedDataUrl);
});
return { dataUrl, mimeType: format };
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ModelCacheManagement.vue:
--------------------------------------------------------------------------------
```vue
<template>
<div class="model-cache-section">
<h2 class="section-title">{{ getMessage('modelCacheManagementLabel') }}</h2>
<!-- Cache Statistics Grid -->
<div class="stats-grid">
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('cacheSizeLabel') }}</p>
<span class="stats-icon orange">
<DatabaseIcon />
</span>
</div>
<p class="stats-value">{{ cacheStats?.totalSizeMB || 0 }} MB</p>
</div>
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('cacheEntriesLabel') }}</p>
<span class="stats-icon purple">
<VectorIcon />
</span>
</div>
<p class="stats-value">{{ cacheStats?.entryCount || 0 }}</p>
</div>
</div>
<!-- Cache Entries Details -->
<div v-if="cacheStats && cacheStats.entries.length > 0" class="cache-details">
<h3 class="cache-details-title">{{ getMessage('cacheDetailsLabel') }}</h3>
<div class="cache-entries">
<div v-for="entry in cacheStats.entries" :key="entry.url" class="cache-entry">
<div class="entry-info">
<div class="entry-url">{{ getModelNameFromUrl(entry.url) }}</div>
<div class="entry-details">
<span class="entry-size">{{ entry.sizeMB }} MB</span>
<span class="entry-age">{{ entry.age }}</span>
<span v-if="entry.expired" class="entry-expired">{{ getMessage('expiredLabel') }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- No Cache Message -->
<div v-else-if="cacheStats && cacheStats.entries.length === 0" class="no-cache">
<p>{{ getMessage('noCacheDataMessage') }}</p>
</div>
<!-- Loading State -->
<div v-else-if="!cacheStats" class="loading-cache">
<p>{{ getMessage('loadingCacheInfoStatus') }}</p>
</div>
<!-- Progress Indicator -->
<ProgressIndicator
v-if="isManagingCache"
:visible="isManagingCache"
:text="isManagingCache ? getMessage('processingCacheStatus') : ''"
:showSpinner="true"
/>
<!-- Action Buttons -->
<div class="cache-actions">
<div class="secondary-button" :disabled="isManagingCache" @click="$emit('cleanup-cache')">
<span class="stats-icon"><DatabaseIcon /></span>
<span>{{
isManagingCache ? getMessage('cleaningStatus') : getMessage('cleanExpiredCacheButton')
}}</span>
</div>
<div class="danger-button" :disabled="isManagingCache" @click="$emit('clear-all-cache')">
<span class="stats-icon"><TrashIcon /></span>
<span>{{ isManagingCache ? getMessage('clearingStatus') : getMessage('clearAllCacheButton') }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import ProgressIndicator from './ProgressIndicator.vue';
import { DatabaseIcon, VectorIcon, TrashIcon } from './icons';
import { getMessage } from '@/utils/i18n';
interface CacheEntry {
url: string;
size: number;
sizeMB: number;
timestamp: number;
age: string;
expired: boolean;
}
interface CacheStats {
totalSize: number;
totalSizeMB: number;
entryCount: number;
entries: CacheEntry[];
}
interface Props {
cacheStats: CacheStats | null;
isManagingCache: boolean;
}
interface Emits {
(e: 'cleanup-cache'): void;
(e: 'clear-all-cache'): void;
}
defineProps<Props>();
defineEmits<Emits>();
const getModelNameFromUrl = (url: string) => {
// Extract model name from HuggingFace URL
const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)/);
if (match) {
return match[1];
}
return url.split('/').pop() || url;
};
</script>
<style scoped>
.model-cache-section {
margin-bottom: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stats-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 16px;
}
.stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.stats-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.stats-icon {
padding: 8px;
border-radius: 8px;
width: 36px;
height: 36px;
}
.stats-icon.orange {
background: #fed7aa;
color: #ea580c;
}
.stats-icon.purple {
background: #e9d5ff;
color: #9333ea;
}
.stats-value {
font-size: 30px;
font-weight: 700;
color: #0f172a;
margin: 0;
}
.cache-details {
margin-bottom: 16px;
}
.cache-details-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 0 0 12px 0;
}
.cache-entries {
display: flex;
flex-direction: column;
gap: 8px;
}
.cache-entry {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
}
.entry-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.entry-url {
font-weight: 500;
color: #1f2937;
font-size: 14px;
}
.entry-details {
display: flex;
gap: 8px;
align-items: center;
font-size: 12px;
}
.entry-size {
background: #dbeafe;
color: #1e40af;
padding: 2px 6px;
border-radius: 4px;
}
.entry-age {
color: #6b7280;
}
.entry-expired {
background: #fee2e2;
color: #dc2626;
padding: 2px 6px;
border-radius: 4px;
}
.no-cache,
.loading-cache {
text-align: center;
color: #6b7280;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
margin-bottom: 16px;
}
.cache-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.secondary-button {
background: #f1f5f9;
color: #475569;
border: 1px solid #cbd5e1;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
user-select: none;
cursor: pointer;
}
.secondary-button:hover:not(:disabled) {
background: #e2e8f0;
border-color: #94a3b8;
}
.secondary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.danger-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: white;
border: 1px solid #d1d5db;
color: #374151;
font-weight: 600;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.danger-button:hover:not(:disabled) {
border-color: #ef4444;
color: #dc2626;
}
.danger-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
```
--------------------------------------------------------------------------------
/app/native-server/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { program } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import {
tryRegisterUserLevelHost,
colorText,
registerWithElevatedPermissions,
ensureExecutionPermissions,
} from './scripts/utils';
import { BrowserType, parseBrowserType, detectInstalledBrowsers } from './scripts/browser-config';
// Import writeNodePath from postinstall
async function writeNodePath(): Promise<void> {
try {
const nodePath = process.execPath;
const nodePathFile = path.join(__dirname, 'node_path.txt');
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
} catch (error: any) {
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
}
}
program
.version(require('../package.json').version)
.description('Mcp Chrome Bridge - Local service for communicating with Chrome extension');
// Register Native Messaging host
program
.command('register')
.description('Register Native Messaging host')
.option('-f, --force', 'Force re-registration')
.option('-s, --system', 'Use system-level installation (requires administrator/sudo privileges)')
.option('-b, --browser <browser>', 'Register for specific browser (chrome, chromium, or all)')
.option('-d, --detect', 'Auto-detect installed browsers')
.action(async (options) => {
try {
// Write Node.js path for run_host scripts
await writeNodePath();
// Determine which browsers to register
let targetBrowsers: BrowserType[] | undefined;
if (options.browser) {
if (options.browser.toLowerCase() === 'all') {
targetBrowsers = [BrowserType.CHROME, BrowserType.CHROMIUM];
console.log(colorText('Registering for all supported browsers...', 'blue'));
} else {
const browserType = parseBrowserType(options.browser);
if (!browserType) {
console.error(
colorText(
`Invalid browser: ${options.browser}. Use 'chrome', 'chromium', or 'all'`,
'red',
),
);
process.exit(1);
}
targetBrowsers = [browserType];
}
} else if (options.detect) {
targetBrowsers = detectInstalledBrowsers();
if (targetBrowsers.length === 0) {
console.log(
colorText(
'No supported browsers detected, will register for Chrome and Chromium',
'yellow',
),
);
targetBrowsers = undefined; // Will use default behavior
}
}
// If neither option specified, tryRegisterUserLevelHost will detect browsers
// Detect if running with root/administrator privileges
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
let isAdmin = false;
if (process.platform === 'win32') {
try {
isAdmin = require('is-admin')(); // Windows requires additional package
} catch (error) {
console.warn(
colorText('Warning: Unable to detect administrator privileges on Windows', 'yellow'),
);
isAdmin = false;
}
}
const hasElevatedPermissions = isRoot || isAdmin;
// If --system option is specified or running with root/administrator privileges
if (options.system || hasElevatedPermissions) {
// TODO: Update registerWithElevatedPermissions to support multiple browsers
await registerWithElevatedPermissions();
console.log(
colorText('System-level Native Messaging host registered successfully!', 'green'),
);
console.log(
colorText(
'You can now use connectNative in Chrome extension to connect to this service.',
'blue',
),
);
} else {
// Regular user-level installation
console.log(colorText('Registering user-level Native Messaging host...', 'blue'));
const success = await tryRegisterUserLevelHost(targetBrowsers);
if (success) {
console.log(colorText('Native Messaging host registered successfully!', 'green'));
console.log(
colorText(
'You can now use connectNative in Chrome extension to connect to this service.',
'blue',
),
);
} else {
console.log(
colorText(
'User-level registration failed, please try the following methods:',
'yellow',
),
);
console.log(colorText(' 1. sudo mcp-chrome-bridge register', 'yellow'));
console.log(colorText(' 2. mcp-chrome-bridge register --system', 'yellow'));
process.exit(1);
}
}
} catch (error: any) {
console.error(colorText(`Registration failed: ${error.message}`, 'red'));
process.exit(1);
}
});
// Fix execution permissions
program
.command('fix-permissions')
.description('Fix execution permissions for native host files')
.action(async () => {
try {
console.log(colorText('Fixing execution permissions...', 'blue'));
await ensureExecutionPermissions();
console.log(colorText('✓ Execution permissions fixed successfully!', 'green'));
} catch (error: any) {
console.error(colorText(`Failed to fix permissions: ${error.message}`, 'red'));
process.exit(1);
}
});
// Update port in stdio-config.json
program
.command('update-port <port>')
.description('Update the port number in stdio-config.json')
.action(async (port: string) => {
try {
const portNumber = parseInt(port, 10);
if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) {
console.error(colorText('Error: Port must be a valid number between 1 and 65535', 'red'));
process.exit(1);
}
const configPath = path.join(__dirname, 'mcp', 'stdio-config.json');
if (!fs.existsSync(configPath)) {
console.error(colorText(`Error: Configuration file not found at ${configPath}`, 'red'));
process.exit(1);
}
const configData = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configData);
const currentUrl = new URL(config.url);
currentUrl.port = portNumber.toString();
config.url = currentUrl.toString();
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
console.log(colorText(`✓ Port updated successfully to ${portNumber}`, 'green'));
console.log(colorText(`Updated URL: ${config.url}`, 'blue'));
} catch (error: any) {
console.error(colorText(`Failed to update port: ${error.message}`, 'red'));
process.exit(1);
}
});
program.parse(process.argv);
// If no command provided, show help
if (!process.argv.slice(2).length) {
program.outputHelp();
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/inject-script.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ExecutionWorld } from '@/common/constants';
interface InjectScriptParam {
url?: string;
}
interface ScriptConfig {
type: ExecutionWorld;
jsScript: string;
}
interface SendCommandToInjectScriptToolParam {
tabId?: number;
eventName: string;
payload?: string;
}
const injectedTabs = new Map();
class InjectScriptTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.INJECT_SCRIPT;
async execute(args: InjectScriptParam & ScriptConfig): Promise<ToolResult> {
try {
const { url, type, jsScript } = args;
let tab;
if (!type || !jsScript) {
return createErrorResponse('Param [type] and [jsScript] is required');
}
if (url) {
// If URL is provided, check if it's already open
console.log(`Checking if URL is already open: ${url}`);
const allTabs = await chrome.tabs.query({});
// Find tab with matching URL
const matchingTabs = allTabs.filter((t) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
if (matchingTabs.length > 0) {
// Use existing tab
tab = matchingTabs[0];
console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
} else {
// Create new tab with the URL
console.log(`No existing tab found with URL: ${url}, creating new tab`);
tab = await chrome.tabs.create({ url, active: true });
// Wait for page to load
console.log('Waiting for page to load...');
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} else {
// Use active tab
const tabs = await chrome.tabs.query({ active: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tab = tabs[0];
}
if (!tab.id) {
return createErrorResponse('Tab has no ID');
}
// Make sure tab is active
await chrome.tabs.update(tab.id, { active: true });
const res = await handleInject(tab.id!, { ...args });
return {
content: [
{
type: 'text',
text: JSON.stringify(res),
},
],
isError: false,
};
} catch (error) {
console.error('Error in InjectScriptTool.execute:', error);
return createErrorResponse(
`Inject script error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
class SendCommandToInjectScriptTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT;
async execute(args: SendCommandToInjectScriptToolParam): Promise<ToolResult> {
try {
const { tabId, eventName, payload } = args;
if (!eventName) {
return createErrorResponse('Param [eventName] is required');
}
if (tabId) {
const tabExists = await isTabExists(tabId);
if (!tabExists) {
return createErrorResponse('The tab:[tabId] is not exists');
}
}
let finalTabId: number | undefined = tabId;
if (finalTabId === undefined) {
// Use active tab
const tabs = await chrome.tabs.query({ active: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
finalTabId = tabs[0].id;
}
if (!finalTabId) {
return createErrorResponse('No active tab found');
}
if (!injectedTabs.has(finalTabId)) {
throw new Error('No script injected in this tab.');
}
const result = await chrome.tabs.sendMessage(finalTabId, {
action: eventName,
payload,
targetWorld: injectedTabs.get(finalTabId).type, // The bridge uses this to decide whether to forward to MAIN world.
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in InjectScriptTool.execute:', error);
return createErrorResponse(
`Inject script error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
async function isTabExists(tabId: number) {
try {
await chrome.tabs.get(tabId);
return true;
} catch (error) {
// An error is thrown if the tab doesn't exist.
return false;
}
}
/**
* @description Handles the injection of user scripts into a specific tab.
* @param {number} tabId - The ID of the target tab.
* @param {object} scriptConfig - The configuration object for the script.
*/
async function handleInject(tabId: number, scriptConfig: ScriptConfig) {
if (injectedTabs.has(tabId)) {
// If already injected, run cleanup first to ensure a clean state.
console.log(`Tab ${tabId} already has injections. Cleaning up first.`);
await handleCleanup(tabId);
}
const { type, jsScript } = scriptConfig;
const hasMain = type === ExecutionWorld.MAIN;
if (hasMain) {
// The bridge is essential for MAIN world communication and cleanup.
await chrome.scripting.executeScript({
target: { tabId },
files: ['inject-scripts/inject-bridge.js'],
world: ExecutionWorld.ISOLATED,
});
await chrome.scripting.executeScript({
target: { tabId },
func: (code) => new Function(code)(),
args: [jsScript],
world: ExecutionWorld.MAIN,
});
} else {
await chrome.scripting.executeScript({
target: { tabId },
func: (code) => new Function(code)(),
args: [jsScript],
world: ExecutionWorld.ISOLATED,
});
}
injectedTabs.set(tabId, scriptConfig);
console.log(`Scripts successfully injected into tab ${tabId}.`);
return { injected: true };
}
/**
* @description Triggers the cleanup process in a specific tab.
* @param {number} tabId - The ID of the target tab.
*/
async function handleCleanup(tabId: number) {
if (!injectedTabs.has(tabId)) return;
// Send cleanup signal. The bridge will forward it to the MAIN world.
chrome.tabs
.sendMessage(tabId, { type: 'chrome-mcp:cleanup' })
.catch((err) =>
console.warn(`Could not send cleanup message to tab ${tabId}. It might have been closed.`),
);
injectedTabs.delete(tabId);
console.log(`Cleanup signal sent to tab ${tabId}. State cleared.`);
}
export const injectScriptTool = new InjectScriptTool();
export const sendCommandToInjectScriptTool = new SendCommandToInjectScriptTool();
// --- Automatic Cleanup Listeners ---
chrome.tabs.onRemoved.addListener((tabId) => {
if (injectedTabs.has(tabId)) {
console.log(`Tab ${tabId} closed. Cleaning up state.`);
injectedTabs.delete(tabId);
}
});
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/click-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
// click-helper.js
// This script is injected into the page to handle click operations
if (window.__CLICK_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__CLICK_HELPER_INITIALIZED__ = true;
/**
* Click on an element matching the selector or at specific coordinates
* @param {string} selector - CSS selector for the element to click
* @param {boolean} waitForNavigation - Whether to wait for navigation to complete after click
* @param {number} timeout - Timeout in milliseconds for waiting for the element or navigation
* @param {Object} coordinates - Optional coordinates for clicking at a specific position
* @param {number} coordinates.x - X coordinate relative to the viewport
* @param {number} coordinates.y - Y coordinate relative to the viewport
* @returns {Promise<Object>} - Result of the click operation
*/
async function clickElement(
selector,
waitForNavigation = false,
timeout = 5000,
coordinates = null,
) {
try {
let element = null;
let elementInfo = null;
let clickX, clickY;
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
clickX = coordinates.x;
clickY = coordinates.y;
element = document.elementFromPoint(clickX, clickY);
if (element) {
const rect = element.getBoundingClientRect();
elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
text: element.textContent?.trim().substring(0, 100) || '',
href: element.href || null,
type: element.type || null,
isVisible: true,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
clickMethod: 'coordinates',
clickPosition: { x: clickX, y: clickY },
};
} else {
elementInfo = {
clickMethod: 'coordinates',
clickPosition: { x: clickX, y: clickY },
warning: 'No element found at the specified coordinates',
};
}
} else {
element = document.querySelector(selector);
if (!element) {
return {
error: `Element with selector "${selector}" not found`,
};
}
const rect = element.getBoundingClientRect();
elementInfo = {
tagName: element.tagName,
id: element.id,
className: element.className,
text: element.textContent?.trim().substring(0, 100) || '',
href: element.href || null,
type: element.type || null,
isVisible: true,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
clickMethod: 'selector',
};
// First sroll so that the element is in view, then check visibility.
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
await new Promise((resolve) => setTimeout(resolve, 100));
elementInfo.isVisible = isElementVisible(element);
if (!elementInfo.isVisible) {
return {
error: `Element with selector "${selector}" is not visible`,
elementInfo,
};
}
const updatedRect = element.getBoundingClientRect();
clickX = updatedRect.left + updatedRect.width / 2;
clickY = updatedRect.top + updatedRect.height / 2;
}
let navigationPromise;
if (waitForNavigation) {
navigationPromise = new Promise((resolve) => {
const beforeUnloadListener = () => {
window.removeEventListener('beforeunload', beforeUnloadListener);
resolve(true);
};
window.addEventListener('beforeunload', beforeUnloadListener);
setTimeout(() => {
window.removeEventListener('beforeunload', beforeUnloadListener);
resolve(false);
}, timeout);
});
}
if (element && elementInfo.clickMethod === 'selector') {
element.click();
} else {
simulateClick(clickX, clickY);
}
// Wait for navigation if needed
let navigationOccurred = false;
if (waitForNavigation) {
navigationOccurred = await navigationPromise;
}
return {
success: true,
message: 'Element clicked successfully',
elementInfo,
navigationOccurred,
};
} catch (error) {
return {
error: `Error clicking element: ${error.message}`,
};
}
}
/**
* Simulate a mouse click at specific coordinates
* @param {number} x - X coordinate relative to the viewport
* @param {number} y - Y coordinate relative to the viewport
*/
function simulateClick(x, y) {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
});
const element = document.elementFromPoint(x, y);
if (element) {
element.dispatchEvent(clickEvent);
} else {
document.dispatchEvent(clickEvent);
}
}
/**
* Check if an element is visible
* @param {Element} element - The element to check
* @returns {boolean} - Whether the element is visible
*/
function isElementVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
if (
rect.bottom < 0 ||
rect.top > window.innerHeight ||
rect.right < 0 ||
rect.left > window.innerWidth
) {
return false;
}
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
if (!elementAtPoint) return false;
return element === elementAtPoint || element.contains(elementAtPoint);
}
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'clickElement') {
clickElement(
request.selector,
request.waitForNavigation,
request.timeout,
request.coordinates,
)
.then(sendResponse)
.catch((error) => {
sendResponse({
error: `Unexpected error: ${error.message}`,
});
});
return true; // Indicates async response
} else if (request.action === 'chrome_click_element_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/history.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import {
parseISO,
subDays,
subWeeks,
subMonths,
subYears,
startOfToday,
startOfYesterday,
isValid,
format,
} from 'date-fns';
interface HistoryToolParams {
text?: string;
startTime?: string;
endTime?: string;
maxResults?: number;
excludeCurrentTabs?: boolean;
}
interface HistoryItem {
id: string;
url?: string;
title?: string;
lastVisitTime?: number; // Timestamp in milliseconds
visitCount?: number;
typedCount?: number;
}
interface HistoryResult {
items: HistoryItem[];
totalCount: number;
timeRange: {
startTime: number;
endTime: number;
startTimeFormatted: string;
endTimeFormatted: string;
};
query?: string;
}
class HistoryTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.HISTORY;
private static readonly ONE_DAY_MS = 24 * 60 * 60 * 1000;
/**
* Parse a date string into milliseconds since epoch.
* Returns null if the date string is invalid.
* Supports:
* - ISO date strings (e.g., "2023-10-31", "2023-10-31T14:30:00.000Z")
* - Relative times: "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"
* - Special keywords: "now", "today", "yesterday"
*/
private parseDateString(dateStr: string | undefined | null): number | null {
if (!dateStr) {
// If an empty or null string is passed, it might mean "no specific date",
// depending on how you want to treat it. Returning null is safer.
return null;
}
const now = new Date();
const lowerDateStr = dateStr.toLowerCase().trim();
if (lowerDateStr === 'now') return now.getTime();
if (lowerDateStr === 'today') return startOfToday().getTime();
if (lowerDateStr === 'yesterday') return startOfYesterday().getTime();
const relativeMatch = lowerDateStr.match(
/^(\d+)\s+(day|days|week|weeks|month|months|year|years)\s+ago$/,
);
if (relativeMatch) {
const amount = parseInt(relativeMatch[1], 10);
const unit = relativeMatch[2];
let resultDate: Date;
if (unit.startsWith('day')) resultDate = subDays(now, amount);
else if (unit.startsWith('week')) resultDate = subWeeks(now, amount);
else if (unit.startsWith('month')) resultDate = subMonths(now, amount);
else if (unit.startsWith('year')) resultDate = subYears(now, amount);
else return null; // Should not happen with the regex
return resultDate.getTime();
}
// Try parsing as ISO or other common date string formats
// Native Date constructor can be unreliable for non-standard formats.
// date-fns' parseISO is good for ISO 8601.
// For other formats, date-fns' parse function is more flexible.
let parsedDate = parseISO(dateStr); // Handles "2023-10-31" or "2023-10-31T10:00:00"
if (isValid(parsedDate)) {
return parsedDate.getTime();
}
// Fallback to new Date() for other potential formats, but with caution
parsedDate = new Date(dateStr);
if (isValid(parsedDate) && dateStr.includes(parsedDate.getFullYear().toString())) {
return parsedDate.getTime();
}
console.warn(`Could not parse date string: ${dateStr}`);
return null;
}
/**
* Format a timestamp as a human-readable date string
*/
private formatDate(timestamp: number): string {
// Using date-fns for consistent and potentially localized formatting
return format(timestamp, 'yyyy-MM-dd HH:mm:ss');
}
async execute(args: HistoryToolParams): Promise<ToolResult> {
try {
console.log('Executing HistoryTool with args:', args);
const {
text = '',
maxResults = 100, // Default to 100 results
excludeCurrentTabs = false,
} = args;
const now = Date.now();
let startTimeMs: number;
let endTimeMs: number;
// Parse startTime
if (args.startTime) {
const parsedStart = this.parseDateString(args.startTime);
if (parsedStart === null) {
return createErrorResponse(
`Invalid format for start time: "${args.startTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
);
}
startTimeMs = parsedStart;
} else {
// Default to 24 hours ago if startTime is not provided
startTimeMs = now - HistoryTool.ONE_DAY_MS;
}
// Parse endTime
if (args.endTime) {
const parsedEnd = this.parseDateString(args.endTime);
if (parsedEnd === null) {
return createErrorResponse(
`Invalid format for end time: "${args.endTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
);
}
endTimeMs = parsedEnd;
} else {
// Default to current time if endTime is not provided
endTimeMs = now;
}
// Validate time range
if (startTimeMs > endTimeMs) {
return createErrorResponse('Start time cannot be after end time.');
}
console.log(
`Searching history from ${this.formatDate(startTimeMs)} to ${this.formatDate(endTimeMs)} for query "${text}"`,
);
const historyItems = await chrome.history.search({
text,
startTime: startTimeMs,
endTime: endTimeMs,
maxResults,
});
console.log(`Found ${historyItems.length} history items before filtering current tabs.`);
let filteredItems = historyItems;
if (excludeCurrentTabs && historyItems.length > 0) {
const currentTabs = await chrome.tabs.query({});
const openUrls = new Set<string>();
currentTabs.forEach((tab) => {
if (tab.url) {
openUrls.add(tab.url);
}
});
if (openUrls.size > 0) {
filteredItems = historyItems.filter((item) => !(item.url && openUrls.has(item.url)));
console.log(
`Filtered out ${historyItems.length - filteredItems.length} items that are currently open. ${filteredItems.length} items remaining.`,
);
}
}
const result: HistoryResult = {
items: filteredItems.map((item) => ({
id: item.id,
url: item.url,
title: item.title,
lastVisitTime: item.lastVisitTime,
visitCount: item.visitCount,
typedCount: item.typedCount,
})),
totalCount: filteredItems.length,
timeRange: {
startTime: startTimeMs,
endTime: endTimeMs,
startTimeFormatted: this.formatDate(startTimeMs),
endTimeFormatted: this.formatDate(endTimeMs),
},
};
if (text) {
result.query = text;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
console.error('Error in HistoryTool.execute:', error);
return createErrorResponse(
`Error retrieving browsing history: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const historyTool = new HistoryTool();
```