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.
@@ -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;