venombrowser 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +26 -0
- package/bin/venombrowser-agent.js +182 -0
- package/bin/venombrowser-chat.js +300 -0
- package/bin/venombrowser-cli.js +194 -0
- package/bin/venombrowser-mcp.js +30 -0
- package/bin/venombrowser-server.js +11 -0
- package/bin/venombrowser-setup.js +183 -0
- package/package.json +65 -0
- package/src/bridge-client.js +207 -0
- package/src/bridge-server.js +476 -0
- package/src/connect.js +220 -0
- package/src/mcp-server.js +101 -0
- package/src/providers/index.js +47 -0
- package/src/providers/ollama.js +108 -0
- package/src/providers/openrouter.js +158 -0
- package/src/tools/browser-tools.js +364 -0
- package/src/uninstall.js +246 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VenomBrowser MCP Server — Kimi-style Model Context Protocol interface
|
|
3
|
+
*
|
|
4
|
+
* Implements MCP (JSON-RPC 2.0 over stdio) for agent integration.
|
|
5
|
+
* Communicates with daemon via single POST /command endpoint.
|
|
6
|
+
*
|
|
7
|
+
* v4.0.0 — Kimi-style architecture:
|
|
8
|
+
* - Uses new bridge-client with Kimi-style protocol
|
|
9
|
+
* - Screenshot returns as MCP image content (not base64 in text)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
13
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
14
|
+
const {
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
CallToolRequestSchema
|
|
17
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
18
|
+
|
|
19
|
+
const { BrowserBridge } = require('./bridge-client');
|
|
20
|
+
const { getMcpToolDefinitions, executeTool } = require('./tools/browser-tools');
|
|
21
|
+
|
|
22
|
+
class VenomBrowserMcpServer {
|
|
23
|
+
constructor() {
|
|
24
|
+
const host = process.env.BRIDGE_HOST || '127.0.0.1';
|
|
25
|
+
const port = parseInt(process.env.BRIDGE_PORT || '10086', 10);
|
|
26
|
+
const session = process.env.BRIDGE_SESSION || 'mcp-session';
|
|
27
|
+
|
|
28
|
+
this.bridge = new BrowserBridge({ host, port, session });
|
|
29
|
+
|
|
30
|
+
this.server = new Server(
|
|
31
|
+
{ name: 'venombrowser', version: '4.0.0' },
|
|
32
|
+
{ capabilities: { tools: {} } }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
this._registerHandlers();
|
|
36
|
+
this._registerErrorHandling();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_registerHandlers() {
|
|
40
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
41
|
+
return { tools: getMcpToolDefinitions() };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
45
|
+
const { name, arguments: args } = request.params;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await executeTool(this.bridge, name, args || {});
|
|
49
|
+
|
|
50
|
+
// Special handling for screenshots — return as MCP image content
|
|
51
|
+
if (name === 'screenshot' && result.path) {
|
|
52
|
+
const fs = require('fs');
|
|
53
|
+
if (fs.existsSync(result.path)) {
|
|
54
|
+
const imageBuffer = fs.readFileSync(result.path);
|
|
55
|
+
const base64 = imageBuffer.toString('base64');
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{ type: 'image', data: base64, mimeType: result.mimeType || 'image/png' },
|
|
59
|
+
{ type: 'text', text: `Screenshot saved to ${result.path} (${result.sizeBytes} bytes)` }
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
71
|
+
isError: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_registerErrorHandling() {
|
|
78
|
+
this.server.onerror = (error) => {
|
|
79
|
+
console.error('[VenomBrowser MCP] Server error:', error);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.on('SIGINT', async () => {
|
|
83
|
+
await this.server.close();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.on('SIGTERM', async () => {
|
|
88
|
+
await this.server.close();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async start() {
|
|
94
|
+
const transport = new StdioServerTransport();
|
|
95
|
+
await this.server.connect(transport);
|
|
96
|
+
console.error('[VenomBrowser MCP] Server started (stdio transport)');
|
|
97
|
+
console.error('[VenomBrowser MCP] Serving browser tools to connected agent');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = VenomBrowserMcpServer;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VenomBrowser LLM Provider Registry
|
|
3
|
+
*
|
|
4
|
+
* Factory pattern for pluggable LLM providers. All providers implement:
|
|
5
|
+
* async chat(messages, tools) → { content, tool_calls }
|
|
6
|
+
*
|
|
7
|
+
* Config via environment variables:
|
|
8
|
+
* LLM_PROVIDER — "ollama" | "openrouter" (default: "openrouter")
|
|
9
|
+
* + provider-specific vars (see individual provider files)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const OllamaProvider = require('./ollama');
|
|
13
|
+
const OpenRouterProvider = require('./openrouter');
|
|
14
|
+
|
|
15
|
+
const PROVIDERS = {
|
|
16
|
+
ollama: OllamaProvider,
|
|
17
|
+
openrouter: OpenRouterProvider
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a provider instance based on config.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} [opts] - Override options
|
|
24
|
+
* @param {string} [opts.provider] - Provider name (overrides LLM_PROVIDER env)
|
|
25
|
+
* @param {object} [opts.config] - Provider-specific config (overrides env vars)
|
|
26
|
+
* @returns {object} Provider instance with .chat(messages, tools) method
|
|
27
|
+
*/
|
|
28
|
+
function createProvider(opts = {}) {
|
|
29
|
+
const name = (opts.provider || process.env.LLM_PROVIDER || 'openrouter').toLowerCase();
|
|
30
|
+
|
|
31
|
+
const ProviderClass = PROVIDERS[name];
|
|
32
|
+
if (!ProviderClass) {
|
|
33
|
+
const available = Object.keys(PROVIDERS).join(', ');
|
|
34
|
+
throw new Error(`Unknown LLM provider "${name}". Available: ${available}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new ProviderClass(opts.config || {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List available provider names.
|
|
42
|
+
*/
|
|
43
|
+
function listProviders() {
|
|
44
|
+
return Object.keys(PROVIDERS);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { createProvider, listProviders };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VenomBrowser — Ollama Provider
|
|
3
|
+
*
|
|
4
|
+
* Talks to a local Ollama instance using /api/chat with tool calling.
|
|
5
|
+
*
|
|
6
|
+
* Config (env vars):
|
|
7
|
+
* OLLAMA_HOST — default http://localhost:11434
|
|
8
|
+
* OLLAMA_MODEL — default qwen2.5:7b
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const axios = require('axios');
|
|
12
|
+
|
|
13
|
+
class OllamaProvider {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.host = config.host || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
16
|
+
this.model = config.model || process.env.OLLAMA_MODEL || 'qwen2.5:7b';
|
|
17
|
+
this.name = 'ollama';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Send a chat completion request to Ollama.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array} messages - Chat messages array
|
|
24
|
+
* @param {Array} tools - Tool definitions (OpenAI format)
|
|
25
|
+
* @returns {Promise<{content: string|null, tool_calls: Array|null}>}
|
|
26
|
+
*/
|
|
27
|
+
async chat(messages, tools = []) {
|
|
28
|
+
const payload = {
|
|
29
|
+
model: this.model,
|
|
30
|
+
messages,
|
|
31
|
+
stream: false
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Only include tools if provided — some Ollama models don't support them
|
|
35
|
+
if (tools && tools.length > 0) {
|
|
36
|
+
payload.tools = tools;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const res = await axios.post(`${this.host}/api/chat`, payload, {
|
|
40
|
+
timeout: 120000
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const msg = res.data.message;
|
|
44
|
+
|
|
45
|
+
// Normalize Ollama's response to our standard format
|
|
46
|
+
return {
|
|
47
|
+
content: msg.content || null,
|
|
48
|
+
tool_calls: this._normalizeToolCalls(msg.tool_calls)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if Ollama is running and the model is available.
|
|
54
|
+
*/
|
|
55
|
+
async healthCheck() {
|
|
56
|
+
const checks = { running: false, model_available: false, model: this.model };
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await axios.get(`${this.host}/api/version`, { timeout: 5000 });
|
|
60
|
+
checks.running = true;
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`Ollama not running on ${this.host}. Start it with: ollama serve`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const models = await axios.get(`${this.host}/api/tags`, { timeout: 10000 });
|
|
67
|
+
const available = models.data.models.map(m => m.name);
|
|
68
|
+
checks.model_available = available.includes(this.model);
|
|
69
|
+
|
|
70
|
+
if (!checks.model_available) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Model ${this.model} not available. Available: ${available.join(', ')}. ` +
|
|
73
|
+
`Pull it with: ollama pull ${this.model}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.message.includes('not available') || err.message.includes('not running')) throw err;
|
|
78
|
+
throw new Error(`Could not check Ollama models: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return checks;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize Ollama tool_calls to standard format.
|
|
86
|
+
* Ollama sometimes returns arguments as string, sometimes as object.
|
|
87
|
+
*/
|
|
88
|
+
_normalizeToolCalls(toolCalls) {
|
|
89
|
+
if (!toolCalls || toolCalls.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
return toolCalls.map(call => ({
|
|
92
|
+
id: call.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
93
|
+
type: 'function',
|
|
94
|
+
function: {
|
|
95
|
+
name: call.function.name,
|
|
96
|
+
arguments: typeof call.function.arguments === 'string'
|
|
97
|
+
? JSON.parse(call.function.arguments)
|
|
98
|
+
: call.function.arguments || {}
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
toString() {
|
|
104
|
+
return `OllamaProvider(${this.host}, model=${this.model})`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = OllamaProvider;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VenomBrowser — OpenRouter Provider
|
|
3
|
+
*
|
|
4
|
+
* Talks to OpenRouter's OpenAI-compatible API for chat completions with
|
|
5
|
+
* tool calling. Works with both free and paid models.
|
|
6
|
+
*
|
|
7
|
+
* Config (env vars):
|
|
8
|
+
* OPENROUTER_API_KEY — your sk-or-v1-... key (required)
|
|
9
|
+
* OPENROUTER_MODEL — model slug, default meta-llama/llama-4-maverick:free
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const axios = require('axios');
|
|
13
|
+
|
|
14
|
+
const OPENROUTER_BASE = 'https://openrouter.ai/api/v1';
|
|
15
|
+
|
|
16
|
+
class OpenRouterProvider {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.apiKey = config.apiKey || process.env.OPENROUTER_API_KEY;
|
|
19
|
+
this.model = config.model || process.env.OPENROUTER_MODEL || 'meta-llama/llama-4-maverick:free';
|
|
20
|
+
this.name = 'openrouter';
|
|
21
|
+
|
|
22
|
+
if (!this.apiKey) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'OPENROUTER_API_KEY is required. Get a free key at https://openrouter.ai/keys'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.http = axios.create({
|
|
29
|
+
baseURL: OPENROUTER_BASE,
|
|
30
|
+
timeout: 120000,
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'HTTP-Referer': 'https://github.com/TechVenom/VenomBrowser',
|
|
35
|
+
'X-Title': 'VenomBrowser Browser Agent'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Send a chat completion request to OpenRouter.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array} messages - Chat messages array
|
|
44
|
+
* @param {Array} tools - Tool definitions (OpenAI format)
|
|
45
|
+
* @returns {Promise<{content: string|null, tool_calls: Array|null}>}
|
|
46
|
+
*/
|
|
47
|
+
async chat(messages, tools = []) {
|
|
48
|
+
const payload = {
|
|
49
|
+
model: this.model,
|
|
50
|
+
messages: this._normalizeMessages(messages),
|
|
51
|
+
stream: false
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Only include tools if provided
|
|
55
|
+
if (tools && tools.length > 0) {
|
|
56
|
+
payload.tools = tools;
|
|
57
|
+
payload.tool_choice = 'auto';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const res = await this.http.post('/chat/completions', payload);
|
|
61
|
+
const choice = res.data.choices?.[0];
|
|
62
|
+
|
|
63
|
+
if (!choice) {
|
|
64
|
+
throw new Error('OpenRouter returned no choices');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const msg = choice.message;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: msg.content || null,
|
|
71
|
+
tool_calls: this._normalizeToolCalls(msg.tool_calls)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if OpenRouter is reachable and the API key is valid.
|
|
77
|
+
*/
|
|
78
|
+
async healthCheck() {
|
|
79
|
+
const checks = { reachable: false, key_valid: false, model: this.model };
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// OpenRouter doesn't have a dedicated health endpoint, but we can
|
|
83
|
+
// hit /models to verify the key works and the model exists.
|
|
84
|
+
const res = await this.http.get('/models', { timeout: 10000 });
|
|
85
|
+
checks.reachable = true;
|
|
86
|
+
checks.key_valid = true;
|
|
87
|
+
|
|
88
|
+
// Check if our model is in the list
|
|
89
|
+
const models = res.data.data || [];
|
|
90
|
+
const found = models.find(m => m.id === this.model);
|
|
91
|
+
if (!found) {
|
|
92
|
+
// Model might still work even if not in the first page of results
|
|
93
|
+
// (OpenRouter has many models), so just warn
|
|
94
|
+
console.warn(`[OpenRouter] Model ${this.model} not found in model list — it may still work.`);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err.response?.status === 401) {
|
|
98
|
+
throw new Error('Invalid OpenRouter API key. Check OPENROUTER_API_KEY.');
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`OpenRouter unreachable: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return checks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalize messages for OpenRouter compatibility.
|
|
108
|
+
* OpenRouter uses OpenAI's format, but we need to ensure tool results
|
|
109
|
+
* are properly formatted.
|
|
110
|
+
*/
|
|
111
|
+
_normalizeMessages(messages) {
|
|
112
|
+
return messages.map(msg => {
|
|
113
|
+
// Tool results need to have tool_call_id in OpenAI format
|
|
114
|
+
if (msg.role === 'tool') {
|
|
115
|
+
return {
|
|
116
|
+
role: 'tool',
|
|
117
|
+
tool_call_id: msg.tool_call_id || msg.name || 'unknown',
|
|
118
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Assistant messages with tool_calls
|
|
123
|
+
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
124
|
+
return {
|
|
125
|
+
role: 'assistant',
|
|
126
|
+
content: msg.content || null,
|
|
127
|
+
tool_calls: msg.tool_calls
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return msg;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Normalize OpenRouter/OpenAI tool_calls to standard format.
|
|
137
|
+
*/
|
|
138
|
+
_normalizeToolCalls(toolCalls) {
|
|
139
|
+
if (!toolCalls || toolCalls.length === 0) return null;
|
|
140
|
+
|
|
141
|
+
return toolCalls.map(call => ({
|
|
142
|
+
id: call.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
143
|
+
type: 'function',
|
|
144
|
+
function: {
|
|
145
|
+
name: call.function.name,
|
|
146
|
+
arguments: typeof call.function.arguments === 'string'
|
|
147
|
+
? JSON.parse(call.function.arguments)
|
|
148
|
+
: call.function.arguments || {}
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
toString() {
|
|
154
|
+
return `OpenRouterProvider(model=${this.model})`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = OpenRouterProvider;
|