browseai-dev 0.2.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/README.md +225 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +616 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +90 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# browseai-dev
|
|
2
|
+
|
|
3
|
+
**Reliable research infrastructure for AI agents.** The research layer your agents are missing.
|
|
4
|
+
|
|
5
|
+
MCP server with real-time web search, evidence extraction, and structured citations. Drop into Claude Desktop, Cursor, Windsurf, LangChain, CrewAI, or any agent pipeline.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Instead of letting your AI hallucinate, `browseai-dev` gives it real-time access to the web with **structured, cited answers**:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Your question → Web search → Neural rerank → Fetch pages → Extract claims → Verify → Cited answer (streamed)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Every answer includes:
|
|
16
|
+
- **Claims** with source URLs, verification status, and consensus level
|
|
17
|
+
- **7-factor confidence score** (0-1) — evidence-based, not LLM self-assessed, auto-calibrated from feedback
|
|
18
|
+
- **Source quotes** verified against actual page text via hybrid BM25 + NLI matching
|
|
19
|
+
- **Atomic claim decomposition** — compound facts split and verified independently
|
|
20
|
+
- **Execution trace** with timing
|
|
21
|
+
- **3 depth modes** — `"fast"` (default), `"thorough"` (auto-retry with rephrased queries), `"deep"` (premium multi-step agentic research: iterative think-search-extract-evaluate cycles with gap analysis, up to 4 steps, targets 0.85 confidence — requires BAI key + sign-in, 3x quota cost, falls back to thorough when exhausted)
|
|
22
|
+
|
|
23
|
+
### Premium Features (with `BROWSE_API_KEY`)
|
|
24
|
+
|
|
25
|
+
Users with a BrowseAI Dev API key (`bai_xxx`) get enhanced verification:
|
|
26
|
+
- **Neural cross-encoder re-ranking** — search results re-scored by semantic query-document relevance
|
|
27
|
+
- **NLI semantic reranking** — evidence matched by meaning, not just keywords
|
|
28
|
+
- **Multi-provider search** — parallel search across multiple sources for broader coverage
|
|
29
|
+
- **Multi-pass consistency** — claims cross-checked across independent extraction passes
|
|
30
|
+
- **Deep reasoning mode** — multi-step agentic research with iterative think-search-extract-evaluate cycles, gap analysis, and cross-step claim merging (up to 4 steps, 3x quota cost, 100 deep queries/day)
|
|
31
|
+
- **Research Sessions** — persistent memory across queries
|
|
32
|
+
|
|
33
|
+
Free BAI key users get a generous daily quota (100 premium queries/day, or ~33 deep queries/day at 3x cost each). When exceeded, queries gracefully fall back to BM25 keyword verification (deep falls back to thorough). Quota resets every 24 hours.
|
|
34
|
+
|
|
35
|
+
**No account needed** — all tools work with BYOK (your own Tavily + OpenRouter keys) with no signup, no limits, and BM25 keyword verification. Sign in at [browseai.dev](https://browseai.dev) for a free BAI key to unlock premium features.
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx browseai-dev setup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This auto-configures Claude Desktop. You'll need:
|
|
44
|
+
- [Tavily API key](https://tavily.com) (free tier available)
|
|
45
|
+
- [OpenRouter API key](https://openrouter.ai)
|
|
46
|
+
|
|
47
|
+
## Manual Setup
|
|
48
|
+
|
|
49
|
+
### Claude Desktop
|
|
50
|
+
|
|
51
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"browseai-dev": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": ["-y", "browseai-dev"],
|
|
59
|
+
"env": {
|
|
60
|
+
"SERP_API_KEY": "tvly-your-key",
|
|
61
|
+
"OPENROUTER_API_KEY": "your-openrouter-key",
|
|
62
|
+
"BROWSE_API_KEY": "bai_xxx"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> `BROWSE_API_KEY` is optional for search/answer but **required for Research Memory (sessions)**. Get one free at [browseai.dev/dashboard](https://browseai.dev/dashboard).
|
|
70
|
+
|
|
71
|
+
### Cursor / Windsurf
|
|
72
|
+
|
|
73
|
+
Add to your MCP settings:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"browseai-dev": {
|
|
78
|
+
"command": "npx",
|
|
79
|
+
"args": ["-y", "browseai-dev"],
|
|
80
|
+
"env": {
|
|
81
|
+
"SERP_API_KEY": "tvly-your-key",
|
|
82
|
+
"OPENROUTER_API_KEY": "your-openrouter-key",
|
|
83
|
+
"BROWSE_API_KEY": "bai_xxx"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> Add `BROWSE_API_KEY` to enable Research Memory (sessions). Get one free at [browseai.dev/dashboard](https://browseai.dev/dashboard).
|
|
90
|
+
|
|
91
|
+
### HTTP Transport
|
|
92
|
+
|
|
93
|
+
Run as an HTTP server for browser-based clients, Smithery, or any HTTP-capable agent:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Start with HTTP transport
|
|
97
|
+
npx browseai-dev --http
|
|
98
|
+
|
|
99
|
+
# Or set the port via environment variable
|
|
100
|
+
MCP_HTTP_PORT=3100 npx browseai-dev --http
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The server exposes:
|
|
104
|
+
- `POST /mcp` — MCP Streamable HTTP endpoint
|
|
105
|
+
- `GET /health` — Health check
|
|
106
|
+
|
|
107
|
+
### Docker
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
docker build -t browseai-dev ./apps/mcp
|
|
111
|
+
docker run -p 3100:3100 -e BROWSE_API_KEY=bai_xxx browseai-dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## MCP Tools
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `browse_search` | Search the web via multi-provider search |
|
|
119
|
+
| `browse_open` | Fetch and parse a page into clean text |
|
|
120
|
+
| `browse_extract` | Extract structured knowledge from a page |
|
|
121
|
+
| `browse_answer` | Full pipeline: search + extract + cite. `depth`: `"fast"`, `"thorough"`, or `"deep"` |
|
|
122
|
+
| `browse_compare` | Compare raw LLM vs evidence-backed answer |
|
|
123
|
+
| `browse_session_create` | Create a research session (persistent memory across queries) |
|
|
124
|
+
| `browse_session_ask` | Research within a session (recalls prior knowledge, stores new claims) |
|
|
125
|
+
| `browse_session_recall` | Query session knowledge without new web searches |
|
|
126
|
+
| `browse_session_share` | Share a session publicly (returns share URL) |
|
|
127
|
+
| `browse_session_knowledge` | Export all claims from a session |
|
|
128
|
+
| `browse_session_fork` | Fork a shared session to continue the research |
|
|
129
|
+
| `browse_feedback` | Submit accuracy feedback on a result |
|
|
130
|
+
|
|
131
|
+
> **Note:** Session tools (`browse_session_*`) require a BrowseAI Dev API key (`bai_xxx`) for identity and ownership. Set `BROWSE_API_KEY` in your env config. BYOK users can use search/answer but cannot use sessions. Get a free API key at [browseai.dev/dashboard](https://browseai.dev/dashboard).
|
|
132
|
+
|
|
133
|
+
## Examples
|
|
134
|
+
|
|
135
|
+
**Quick lookup:**
|
|
136
|
+
> *"Use browse_answer to explain what causes aurora borealis"*
|
|
137
|
+
|
|
138
|
+
**Higher accuracy:**
|
|
139
|
+
> *"Use browse_answer with depth thorough to research quantum computing"*
|
|
140
|
+
|
|
141
|
+
**Deep research (multi-step, requires BAI key):**
|
|
142
|
+
> *"Use browse_answer with depth deep to compare CRISPR approaches for sickle cell disease"*
|
|
143
|
+
>
|
|
144
|
+
> Deep mode runs iterative think-search-extract-evaluate cycles: gap analysis identifies missing info, follow-up queries fill the gaps, and claims/sources are merged across steps with final re-verification. Targets 0.85 confidence across up to 4 steps. Falls back to thorough without a BAI key or when quota is exhausted.
|
|
145
|
+
|
|
146
|
+
**Contradiction detection:**
|
|
147
|
+
> *"Use browse_answer with depth thorough to check if coffee is good for health, and show me any contradictions"*
|
|
148
|
+
|
|
149
|
+
**Research session:**
|
|
150
|
+
> *"Create a session called quantum-research, then ask about quantum entanglement, then ask how entanglement is used in computing"*
|
|
151
|
+
|
|
152
|
+
**Enterprise search:**
|
|
153
|
+
> *"Use browse_answer to search our Elasticsearch at https://es.company.com/kb/_search for our refund policy"*
|
|
154
|
+
|
|
155
|
+
### Response structure
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"answer": "Aurora borealis occurs when charged particles from the Sun...",
|
|
160
|
+
"confidence": 0.92,
|
|
161
|
+
"claims": [
|
|
162
|
+
{
|
|
163
|
+
"claim": "Aurora borealis is caused by solar wind particles...",
|
|
164
|
+
"sources": ["https://en.wikipedia.org/wiki/Aurora"],
|
|
165
|
+
"verified": true,
|
|
166
|
+
"verificationScore": 0.82,
|
|
167
|
+
"consensusLevel": "strong"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"sources": [
|
|
171
|
+
{
|
|
172
|
+
"url": "https://en.wikipedia.org/wiki/Aurora",
|
|
173
|
+
"title": "Aurora - Wikipedia",
|
|
174
|
+
"domain": "en.wikipedia.org",
|
|
175
|
+
"quote": "An aurora is a natural light display...",
|
|
176
|
+
"verified": true,
|
|
177
|
+
"authority": 0.70
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"contradictions": [],
|
|
181
|
+
"reasoningSteps": []
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Why browseai-dev?
|
|
186
|
+
|
|
187
|
+
| Feature | Raw LLM | browseai-dev |
|
|
188
|
+
|---------|---------|-----------|
|
|
189
|
+
| Sources | None | Real URLs with quotes |
|
|
190
|
+
| Citations | Hallucinated | Verified from pages |
|
|
191
|
+
| Confidence | Unknown | 7-factor evidence-based score |
|
|
192
|
+
| Depth | Single pass | 3 modes: fast, thorough, deep reasoning |
|
|
193
|
+
| Freshness | Training data | Real-time web |
|
|
194
|
+
| Claims | Mixed in text | Structured + linked |
|
|
195
|
+
|
|
196
|
+
## Reliability
|
|
197
|
+
|
|
198
|
+
All API calls include automatic retry with exponential backoff on transient failures (429 rate limits, 5xx server errors). Auth errors fail immediately — no wasted retries.
|
|
199
|
+
|
|
200
|
+
## Tech Stack
|
|
201
|
+
|
|
202
|
+
- **Search**: Multi-provider (parallel search across sources)
|
|
203
|
+
- **Parsing**: @mozilla/readability + linkedom
|
|
204
|
+
- **AI**: OpenRouter (100+ models)
|
|
205
|
+
- **Verification**: Hybrid BM25 + NLI semantic entailment
|
|
206
|
+
- **Protocol**: Model Context Protocol (MCP)
|
|
207
|
+
|
|
208
|
+
## Agent Skills
|
|
209
|
+
|
|
210
|
+
Pre-built skills that teach coding agents when to use BrowseAI Dev tools:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx skills add BrowseAI-HQ/browseAIDev_Skills
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Skills work with Claude Code, Codex CLI, Gemini CLI, Cursor, and more. [View skills →](https://github.com/BrowseAI-HQ/browseAIDev_Skills)
|
|
217
|
+
|
|
218
|
+
## Community
|
|
219
|
+
|
|
220
|
+
- [Discord](https://discord.gg/ubAuT4YQsT)
|
|
221
|
+
- [GitHub](https://github.com/BrowseAI-HQ/BrowseAI-Dev)
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Readability } from "@mozilla/readability";
|
|
7
|
+
import { parseHTML } from "linkedom";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
// --- Constants (inlined for standalone npm package) ---
|
|
11
|
+
const VERSION = "0.2.0";
|
|
12
|
+
const LLM_MODEL = "google/gemini-2.5-flash";
|
|
13
|
+
const LLM_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
|
|
14
|
+
const TAVILY_ENDPOINT = "https://api.tavily.com/search";
|
|
15
|
+
const MAX_PAGE_CONTENT_LENGTH = 3000;
|
|
16
|
+
// --- API mode (BrowseAI Dev API key) ---
|
|
17
|
+
const BROWSE_API_KEY = process.env.BROWSE_API_KEY;
|
|
18
|
+
const BROWSE_API_URL = process.env.BROWSE_API_URL || "https://browseai.dev/api";
|
|
19
|
+
const API_MODE = !!BROWSE_API_KEY;
|
|
20
|
+
// --- CLI handling ---
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.log(`
|
|
24
|
+
browseai-dev v${VERSION}
|
|
25
|
+
Open-source deep research MCP server for AI agents
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
browseai-dev Start the MCP server (stdio transport)
|
|
29
|
+
browseai-dev --http Start the MCP server (HTTP transport)
|
|
30
|
+
browseai-dev setup Auto-configure Claude Desktop
|
|
31
|
+
browseai-dev --help Show this help
|
|
32
|
+
browseai-dev --version Show version
|
|
33
|
+
|
|
34
|
+
Environment Variables:
|
|
35
|
+
BROWSE_API_KEY BrowseAI Dev API key (get one at https://browseai.dev/dashboard)
|
|
36
|
+
SERP_API_KEY Tavily API key (get one at https://tavily.com) — BYOK mode
|
|
37
|
+
OPENROUTER_API_KEY OpenRouter API key (get one at https://openrouter.ai) — BYOK mode
|
|
38
|
+
MCP_HTTP_PORT Port for HTTP transport (default: 3100)
|
|
39
|
+
|
|
40
|
+
MCP Tools:
|
|
41
|
+
browse_search Search the web for information
|
|
42
|
+
browse_open Fetch and parse a web page
|
|
43
|
+
browse_extract Extract structured knowledge from a page
|
|
44
|
+
browse_answer Full pipeline: search + extract + answer
|
|
45
|
+
browse_compare Compare raw LLM vs evidence-backed answer
|
|
46
|
+
browse_session_create Create a research session (persistent memory)
|
|
47
|
+
browse_session_ask Research within a session (recalls prior knowledge)
|
|
48
|
+
browse_session_recall Query session knowledge without new searches
|
|
49
|
+
browse_session_share Share a session publicly via URL
|
|
50
|
+
browse_session_knowledge Export all knowledge from a session
|
|
51
|
+
|
|
52
|
+
Quick Setup:
|
|
53
|
+
Option A: Use a BrowseAI Dev API key (one key for everything)
|
|
54
|
+
1. Sign in at https://browseai.dev and generate an API key
|
|
55
|
+
2. Run: npx browseai-dev setup
|
|
56
|
+
3. Restart Claude Desktop
|
|
57
|
+
|
|
58
|
+
Option B: Bring your own keys (BYOK)
|
|
59
|
+
1. Get API keys: https://tavily.com + https://openrouter.ai
|
|
60
|
+
2. Run: npx browseai-dev setup
|
|
61
|
+
3. Restart Claude Desktop
|
|
62
|
+
`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
66
|
+
console.log(VERSION);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
if (args[0] === "setup") {
|
|
70
|
+
import("./setup.js").then((m) => m.runSetup());
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// --- Start MCP server ---
|
|
74
|
+
startServer();
|
|
75
|
+
}
|
|
76
|
+
async function apiCall(path, body) {
|
|
77
|
+
const res = await fetch(`${BROWSE_API_URL}${path}`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"X-API-Key": BROWSE_API_KEY,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
if (!res.ok || !data.success)
|
|
87
|
+
throw new Error(data.error || `API failed: ${res.status}`);
|
|
88
|
+
// Include quota info in result if present
|
|
89
|
+
if (data.quota) {
|
|
90
|
+
return { ...data.result, _quota: data.quota };
|
|
91
|
+
}
|
|
92
|
+
return data.result;
|
|
93
|
+
}
|
|
94
|
+
// --- Env validation ---
|
|
95
|
+
function getEnvKeys() {
|
|
96
|
+
if (API_MODE)
|
|
97
|
+
return { SERP_API_KEY: "", OPENROUTER_API_KEY: "" };
|
|
98
|
+
const SERP_API_KEY = process.env.SERP_API_KEY;
|
|
99
|
+
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
|
|
100
|
+
if (!SERP_API_KEY || !OPENROUTER_API_KEY) {
|
|
101
|
+
console.error(`
|
|
102
|
+
browseai-dev: Missing required environment variables
|
|
103
|
+
|
|
104
|
+
${!SERP_API_KEY ? " SERP_API_KEY - Get one at https://tavily.com" : " SERP_API_KEY - Set"}
|
|
105
|
+
${!OPENROUTER_API_KEY ? " OPENROUTER_API_KEY - Get one at https://openrouter.ai" : " OPENROUTER_API_KEY - Set"}
|
|
106
|
+
|
|
107
|
+
Quick fix: run 'npx browseai-dev setup' to configure automatically.
|
|
108
|
+
Or use a BrowseAI Dev API key: BROWSE_API_KEY=bai_xxx npx browseai-dev
|
|
109
|
+
`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
return { SERP_API_KEY, OPENROUTER_API_KEY };
|
|
113
|
+
}
|
|
114
|
+
// --- In-memory cache ---
|
|
115
|
+
const cache = new Map();
|
|
116
|
+
function cacheGet(key) {
|
|
117
|
+
const entry = cache.get(key);
|
|
118
|
+
if (!entry || Date.now() > entry.expires) {
|
|
119
|
+
cache.delete(key);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return entry.value;
|
|
123
|
+
}
|
|
124
|
+
function cacheSet(key, value, ttl = 300) {
|
|
125
|
+
cache.set(key, { value, expires: Date.now() + ttl * 1000 });
|
|
126
|
+
}
|
|
127
|
+
// --- Tavily search ---
|
|
128
|
+
async function tavilySearch(query, limit = 5) {
|
|
129
|
+
const { SERP_API_KEY } = getEnvKeys();
|
|
130
|
+
const cached = cacheGet(`search:${query}:${limit}`);
|
|
131
|
+
if (cached)
|
|
132
|
+
return JSON.parse(cached);
|
|
133
|
+
const res = await fetch(TAVILY_ENDPOINT, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
api_key: SERP_API_KEY,
|
|
138
|
+
query,
|
|
139
|
+
max_results: limit,
|
|
140
|
+
include_raw_content: false,
|
|
141
|
+
search_depth: "basic",
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok)
|
|
145
|
+
throw new Error(`Tavily search failed: ${res.status}`);
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
const results = data.results.map((r) => ({
|
|
148
|
+
url: r.url,
|
|
149
|
+
title: r.title,
|
|
150
|
+
snippet: r.content,
|
|
151
|
+
score: r.score,
|
|
152
|
+
}));
|
|
153
|
+
cacheSet(`search:${query}:${limit}`, JSON.stringify(results), 600);
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
// --- Readability page fetch ---
|
|
157
|
+
async function fetchPage(url) {
|
|
158
|
+
const cached = cacheGet(`page:${url}`);
|
|
159
|
+
if (cached)
|
|
160
|
+
return JSON.parse(cached);
|
|
161
|
+
const res = await fetch(url, {
|
|
162
|
+
headers: {
|
|
163
|
+
"User-Agent": "Mozilla/5.0 (compatible; BrowseAI-Dev/1.0)",
|
|
164
|
+
Accept: "text/html,application/xhtml+xml",
|
|
165
|
+
},
|
|
166
|
+
signal: AbortSignal.timeout(10000),
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok)
|
|
169
|
+
throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
170
|
+
const html = await res.text();
|
|
171
|
+
const { document } = parseHTML(html);
|
|
172
|
+
const reader = new Readability(document);
|
|
173
|
+
const article = reader.parse();
|
|
174
|
+
if (!article)
|
|
175
|
+
throw new Error(`Could not parse ${url}`);
|
|
176
|
+
const page = {
|
|
177
|
+
title: article.title,
|
|
178
|
+
content: (article.textContent ?? "").slice(0, MAX_PAGE_CONTENT_LENGTH * 2),
|
|
179
|
+
excerpt: article.excerpt || "",
|
|
180
|
+
siteName: article.siteName,
|
|
181
|
+
};
|
|
182
|
+
cacheSet(`page:${url}`, JSON.stringify(page), 1800);
|
|
183
|
+
return page;
|
|
184
|
+
}
|
|
185
|
+
// --- LLM knowledge extraction (via OpenRouter) ---
|
|
186
|
+
async function extractKnowledge(query, pageContents) {
|
|
187
|
+
const { OPENROUTER_API_KEY } = getEnvKeys();
|
|
188
|
+
const res = await fetch(LLM_ENDPOINT, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
model: LLM_MODEL,
|
|
196
|
+
messages: [
|
|
197
|
+
{
|
|
198
|
+
role: "system",
|
|
199
|
+
content: "You are a knowledge extraction engine. Given web page content, extract structured claims with source attribution and write a clear answer. Use only extracted evidence. Never invent sources. Preserve citations. Return a JSON object using the tool provided.",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
role: "user",
|
|
203
|
+
content: `Question: ${query}\n\nWeb sources:\n${pageContents}`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
tools: [
|
|
207
|
+
{
|
|
208
|
+
type: "function",
|
|
209
|
+
function: {
|
|
210
|
+
name: "return_knowledge",
|
|
211
|
+
description: "Return extracted knowledge with claims, sources, answer, and confidence",
|
|
212
|
+
parameters: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
answer: { type: "string" },
|
|
216
|
+
confidence: { type: "number" },
|
|
217
|
+
claims: {
|
|
218
|
+
type: "array",
|
|
219
|
+
items: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
claim: { type: "string" },
|
|
223
|
+
sources: {
|
|
224
|
+
type: "array",
|
|
225
|
+
items: { type: "string" },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
required: ["claim", "sources"],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
sources: {
|
|
232
|
+
type: "array",
|
|
233
|
+
items: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
url: { type: "string" },
|
|
237
|
+
title: { type: "string" },
|
|
238
|
+
domain: { type: "string" },
|
|
239
|
+
quote: { type: "string" },
|
|
240
|
+
},
|
|
241
|
+
required: ["url", "title", "domain", "quote"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
required: ["answer", "confidence", "claims", "sources"],
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
tool_choice: {
|
|
252
|
+
type: "function",
|
|
253
|
+
function: { name: "return_knowledge" },
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
if (!res.ok)
|
|
258
|
+
throw new Error(`LLM failed: ${res.status}`);
|
|
259
|
+
const data = await res.json();
|
|
260
|
+
const toolCall = data.choices?.[0]?.message?.tool_calls?.[0];
|
|
261
|
+
if (!toolCall)
|
|
262
|
+
throw new Error("LLM did not return structured output");
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(toolCall.function.arguments);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
throw new Error(`Failed to parse LLM tool call arguments: ${e.message}. Raw: ${toolCall.function.arguments}`, { cause: e });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// --- Raw LLM call (no sources, for compare) ---
|
|
271
|
+
async function rawLLMAnswer(query) {
|
|
272
|
+
const { OPENROUTER_API_KEY } = getEnvKeys();
|
|
273
|
+
const res = await fetch(LLM_ENDPOINT, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: {
|
|
276
|
+
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
model: LLM_MODEL,
|
|
281
|
+
messages: [
|
|
282
|
+
{
|
|
283
|
+
role: "system",
|
|
284
|
+
content: "Answer the question clearly and concisely.",
|
|
285
|
+
},
|
|
286
|
+
{ role: "user", content: query },
|
|
287
|
+
],
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
if (!res.ok)
|
|
291
|
+
throw new Error(`LLM failed: ${res.status}`);
|
|
292
|
+
const data = await res.json();
|
|
293
|
+
return data.choices?.[0]?.message?.content || "No response";
|
|
294
|
+
}
|
|
295
|
+
async function answerPipeline(query) {
|
|
296
|
+
const trace = [];
|
|
297
|
+
const searchStart = Date.now();
|
|
298
|
+
const searchResults = await tavilySearch(query);
|
|
299
|
+
trace.push({
|
|
300
|
+
step: "Search Web",
|
|
301
|
+
duration_ms: Date.now() - searchStart,
|
|
302
|
+
detail: `${searchResults.length} results`,
|
|
303
|
+
});
|
|
304
|
+
const scrapeStart = Date.now();
|
|
305
|
+
const pages = await Promise.allSettled(searchResults.slice(0, 5).map((r) => fetchPage(r.url)));
|
|
306
|
+
const successfulPages = pages
|
|
307
|
+
.filter((p) => p.status === "fulfilled")
|
|
308
|
+
.map((p) => p.value);
|
|
309
|
+
trace.push({
|
|
310
|
+
step: "Fetch Pages",
|
|
311
|
+
duration_ms: Date.now() - scrapeStart,
|
|
312
|
+
detail: `${successfulPages.length} pages`,
|
|
313
|
+
});
|
|
314
|
+
const pageContents = successfulPages
|
|
315
|
+
.map((p, i) => `[Source ${i + 1}] URL: ${searchResults[i]?.url}\nTitle: ${p.title}\n\n${p.content.slice(0, MAX_PAGE_CONTENT_LENGTH)}`)
|
|
316
|
+
.join("\n\n---\n\n");
|
|
317
|
+
const llmStart = Date.now();
|
|
318
|
+
const knowledge = await extractKnowledge(query, pageContents);
|
|
319
|
+
const llmDuration = Date.now() - llmStart;
|
|
320
|
+
trace.push({
|
|
321
|
+
step: "Extract Claims",
|
|
322
|
+
duration_ms: Math.round(llmDuration * 0.4),
|
|
323
|
+
detail: `${knowledge.claims?.length || 0} claims`,
|
|
324
|
+
});
|
|
325
|
+
trace.push({
|
|
326
|
+
step: "Build Evidence Graph",
|
|
327
|
+
duration_ms: Math.round(llmDuration * 0.1),
|
|
328
|
+
detail: `${knowledge.sources?.length || 0} sources`,
|
|
329
|
+
});
|
|
330
|
+
trace.push({
|
|
331
|
+
step: "Generate Answer",
|
|
332
|
+
duration_ms: Math.round(llmDuration * 0.5),
|
|
333
|
+
detail: "OpenRouter",
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
answer: knowledge.answer,
|
|
337
|
+
claims: knowledge.claims || [],
|
|
338
|
+
sources: knowledge.sources || [],
|
|
339
|
+
confidence: knowledge.confidence || 0.85,
|
|
340
|
+
trace,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// --- Tool registration (shared between stdio and http) ---
|
|
344
|
+
function registerTools(server) {
|
|
345
|
+
server.tool("browse_search", "Search the web for information on a topic. Returns URLs, titles, snippets, and relevance scores.", { query: z.string(), limit: z.number().optional() }, async ({ query, limit }) => {
|
|
346
|
+
if (API_MODE) {
|
|
347
|
+
const result = await apiCall("/browse/search", { query, limit: limit ?? 5 });
|
|
348
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
349
|
+
}
|
|
350
|
+
const results = await tavilySearch(query, limit ?? 5);
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
server.tool("browse_open", "Fetch and parse a web page into clean text using Readability. Strips ads, nav, and boilerplate.", { url: z.string() }, async ({ url }) => {
|
|
356
|
+
if (API_MODE) {
|
|
357
|
+
const result = await apiCall("/browse/open", { url });
|
|
358
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
359
|
+
}
|
|
360
|
+
const page = await fetchPage(url);
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }],
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
server.tool("browse_extract", "Extract structured knowledge (claims + sources + confidence) from a single web page using AI.", { url: z.string(), query: z.string().optional() }, async ({ url, query }) => {
|
|
366
|
+
if (API_MODE) {
|
|
367
|
+
const result = await apiCall("/browse/extract", { url, query });
|
|
368
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
369
|
+
}
|
|
370
|
+
const page = await fetchPage(url);
|
|
371
|
+
const domain = new URL(url).hostname;
|
|
372
|
+
const pageContent = `[Source 1] URL: ${url}\nTitle: ${page.title}\n\n${page.content.slice(0, MAX_PAGE_CONTENT_LENGTH)}`;
|
|
373
|
+
const q = query || `Summarize the content from ${domain}`;
|
|
374
|
+
const result = await extractKnowledge(q, pageContent);
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
server.tool("browse_answer", "Full deep research pipeline: search the web, fetch pages, extract claims, build evidence graph, and generate a structured answer with citations and confidence score. Use depth='thorough' for auto-retry with rephrased queries when confidence is low. Use depth='deep' for multi-step agentic research that identifies knowledge gaps and runs follow-up searches. Enterprise: use searchProvider to search internal data instead of the public web.", {
|
|
380
|
+
query: z.string(),
|
|
381
|
+
depth: z.enum(["fast", "thorough", "deep"]).optional().describe("Research depth: 'fast' (default), 'thorough' (auto-retry if confidence < 60%), or 'deep' (multi-step agentic research with gap analysis)"),
|
|
382
|
+
searchProvider: z.object({
|
|
383
|
+
type: z.enum(["tavily", "brave", "elasticsearch", "confluence", "custom"]).describe("Search backend type"),
|
|
384
|
+
endpoint: z.string().optional().describe("Endpoint URL (required for elasticsearch, confluence, custom)"),
|
|
385
|
+
authHeader: z.string().optional().describe("Auth header value (e.g. 'Bearer xxx')"),
|
|
386
|
+
index: z.string().optional().describe("Elasticsearch index name"),
|
|
387
|
+
spaceKey: z.string().optional().describe("Confluence space key"),
|
|
388
|
+
dataRetention: z.enum(["normal", "none"]).optional().describe("'none' skips all caching/storage (enterprise)"),
|
|
389
|
+
}).optional().describe("Enterprise: configure a custom search backend instead of public web search"),
|
|
390
|
+
}, async ({ query, depth, searchProvider }) => {
|
|
391
|
+
if (API_MODE) {
|
|
392
|
+
const body = { query, depth: depth || "fast" };
|
|
393
|
+
if (searchProvider)
|
|
394
|
+
body.searchProvider = searchProvider;
|
|
395
|
+
const result = await apiCall("/browse/answer", body);
|
|
396
|
+
const content = [
|
|
397
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
398
|
+
];
|
|
399
|
+
// Surface quota info so agents can inform users about premium status
|
|
400
|
+
if (result._quota) {
|
|
401
|
+
const q = result._quota;
|
|
402
|
+
const status = q.premiumActive
|
|
403
|
+
? `Premium active (${q.used}/${q.limit} queries used today)`
|
|
404
|
+
: `Premium quota exceeded (${q.used}/${q.limit}). Results use standard verification. Upgrade or wait 24h for reset.`;
|
|
405
|
+
content.push({ type: "text", text: `\n---\nQuota: ${status}` });
|
|
406
|
+
}
|
|
407
|
+
return { content };
|
|
408
|
+
}
|
|
409
|
+
const result = await answerPipeline(query);
|
|
410
|
+
let text = JSON.stringify(result, null, 2);
|
|
411
|
+
if (depth && depth !== "fast") {
|
|
412
|
+
text += `\n\n> Note: depth="${depth}" requested but BYOK mode uses standard search depth. Use a BrowseAI API key for thorough/deep modes.`;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text }],
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
server.tool("browse_compare", "Compare a raw LLM answer (no sources) vs an evidence-backed answer. Shows the difference between hallucination-prone and grounded responses.", { query: z.string() }, async ({ query }) => {
|
|
419
|
+
if (API_MODE) {
|
|
420
|
+
const result = await apiCall("/browse/compare", { query });
|
|
421
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
422
|
+
}
|
|
423
|
+
const [rawAnswer, evidenceResult] = await Promise.all([
|
|
424
|
+
rawLLMAnswer(query),
|
|
425
|
+
answerPipeline(query),
|
|
426
|
+
]);
|
|
427
|
+
const comparison = {
|
|
428
|
+
query,
|
|
429
|
+
raw_llm: {
|
|
430
|
+
answer: rawAnswer,
|
|
431
|
+
sources: 0,
|
|
432
|
+
claims: 0,
|
|
433
|
+
confidence: null,
|
|
434
|
+
},
|
|
435
|
+
evidence_backed: {
|
|
436
|
+
answer: evidenceResult.answer,
|
|
437
|
+
sources: evidenceResult.sources.length,
|
|
438
|
+
claims: evidenceResult.claims.length,
|
|
439
|
+
confidence: evidenceResult.confidence,
|
|
440
|
+
citations: evidenceResult.sources,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
return {
|
|
444
|
+
content: [
|
|
445
|
+
{ type: "text", text: JSON.stringify(comparison, null, 2) },
|
|
446
|
+
],
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
// --- Research Memory tools (API mode only — sessions require Supabase) ---
|
|
450
|
+
server.tool("browse_session_create", "Create a new research session. Sessions persist knowledge across multiple queries — each query builds on prior research.", { name: z.string().describe("Name for the session (e.g. 'wasm-research', 'react-comparison')") }, async ({ name }) => {
|
|
451
|
+
if (!API_MODE) {
|
|
452
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key. Set BROWSE_API_KEY to use sessions." }] };
|
|
453
|
+
}
|
|
454
|
+
const result = await apiCall("/session", { name });
|
|
455
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
456
|
+
});
|
|
457
|
+
server.tool("browse_session_ask", "Research a question within a session. Recalls prior knowledge, runs the research pipeline, and stores new claims. Later queries in the same session benefit from accumulated knowledge.", {
|
|
458
|
+
session_id: z.string().describe("Session ID from browse_session_create"),
|
|
459
|
+
query: z.string(),
|
|
460
|
+
depth: z.enum(["fast", "thorough", "deep"]).optional().describe("'fast' (default), 'thorough', or 'deep' (multi-step agentic)"),
|
|
461
|
+
}, async ({ session_id, query, depth }) => {
|
|
462
|
+
if (!API_MODE) {
|
|
463
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key." }] };
|
|
464
|
+
}
|
|
465
|
+
const result = await apiCall(`/session/${session_id}/ask`, { query, depth: depth || "fast" });
|
|
466
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
467
|
+
});
|
|
468
|
+
server.tool("browse_session_recall", "Query accumulated knowledge from a session without making new web searches. Returns previously verified claims relevant to the query.", {
|
|
469
|
+
session_id: z.string().describe("Session ID"),
|
|
470
|
+
query: z.string().describe("What to recall from session knowledge"),
|
|
471
|
+
limit: z.number().optional().describe("Max entries to return (default 10)"),
|
|
472
|
+
}, async ({ session_id, query, limit }) => {
|
|
473
|
+
if (!API_MODE) {
|
|
474
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key." }] };
|
|
475
|
+
}
|
|
476
|
+
const result = await apiCall(`/session/${session_id}/recall`, { query, limit: limit ?? 10 });
|
|
477
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
478
|
+
});
|
|
479
|
+
server.tool("browse_session_share", "Share a research session publicly. Returns a shareable URL that anyone can view — great for sharing research findings with teammates, in reports, or on social media.", {
|
|
480
|
+
session_id: z.string().describe("Session ID to share"),
|
|
481
|
+
}, async ({ session_id }) => {
|
|
482
|
+
if (!API_MODE) {
|
|
483
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key." }] };
|
|
484
|
+
}
|
|
485
|
+
const result = await apiCall(`/session/${session_id}/share`, {});
|
|
486
|
+
const shareUrl = `https://browseai.dev/session/share/${result.shareId}`;
|
|
487
|
+
return {
|
|
488
|
+
content: [{
|
|
489
|
+
type: "text",
|
|
490
|
+
text: JSON.stringify({ shareId: result.shareId, url: shareUrl, message: "Session shared! Anyone with this link can view the research." }, null, 2),
|
|
491
|
+
}],
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
server.tool("browse_session_knowledge", "Export all knowledge from a research session. Returns all verified claims, sources, and confidence scores accumulated across queries.", {
|
|
495
|
+
session_id: z.string().describe("Session ID"),
|
|
496
|
+
limit: z.number().optional().describe("Max entries to return (default 50)"),
|
|
497
|
+
}, async ({ session_id, limit }) => {
|
|
498
|
+
if (!API_MODE) {
|
|
499
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key." }] };
|
|
500
|
+
}
|
|
501
|
+
const res = await fetch(`${BROWSE_API_URL}/session/${session_id}/knowledge?limit=${limit ?? 50}`, {
|
|
502
|
+
headers: { "X-API-Key": BROWSE_API_KEY },
|
|
503
|
+
});
|
|
504
|
+
const data = await res.json();
|
|
505
|
+
if (!data.success)
|
|
506
|
+
throw new Error(data.error || "Failed to export knowledge");
|
|
507
|
+
return { content: [{ type: "text", text: JSON.stringify(data.result, null, 2) }] };
|
|
508
|
+
});
|
|
509
|
+
server.tool("browse_session_fork", "Fork a shared research session to continue building on someone else's research. Creates a copy of all knowledge in your own session.", {
|
|
510
|
+
share_id: z.string().describe("Share ID from a shared session URL"),
|
|
511
|
+
}, async ({ share_id }) => {
|
|
512
|
+
if (!API_MODE) {
|
|
513
|
+
return { content: [{ type: "text", text: "Research Memory requires a BrowseAI Dev API key." }] };
|
|
514
|
+
}
|
|
515
|
+
const result = await apiCall(`/session/share/${share_id}/fork`, {});
|
|
516
|
+
return {
|
|
517
|
+
content: [{
|
|
518
|
+
type: "text",
|
|
519
|
+
text: JSON.stringify({
|
|
520
|
+
sessionId: result.session.id,
|
|
521
|
+
name: result.session.name,
|
|
522
|
+
claimsForked: result.claimsForked,
|
|
523
|
+
message: "Session forked! You can now continue researching with all the prior knowledge.",
|
|
524
|
+
}, null, 2),
|
|
525
|
+
}],
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
// --- Feedback Tool ---
|
|
529
|
+
server.tool("browse_feedback", "Submit feedback on a search result to improve future accuracy. Helps the self-learning engine tune verification thresholds.", {
|
|
530
|
+
result_id: z.string().describe("The shareId/resultId from a previous search result"),
|
|
531
|
+
rating: z.enum(["good", "bad", "wrong"]).describe("Rate the result: 'good' (accurate), 'bad' (not helpful), or 'wrong' (factually incorrect)"),
|
|
532
|
+
claim_index: z.number().int().min(0).optional().describe("Optional: index of the specific claim that was wrong"),
|
|
533
|
+
}, async ({ result_id, rating, claim_index }) => {
|
|
534
|
+
if (!API_MODE) {
|
|
535
|
+
return { content: [{ type: "text", text: "Feedback requires a BrowseAI API key (BROWSE_API_KEY). Set it to enable feedback." }] };
|
|
536
|
+
}
|
|
537
|
+
const body = { resultId: result_id, rating };
|
|
538
|
+
if (claim_index !== undefined)
|
|
539
|
+
body.claimIndex = claim_index;
|
|
540
|
+
const result = await apiCall("/browse/feedback", body);
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: "text",
|
|
544
|
+
text: JSON.stringify({ recorded: true, message: "Feedback recorded. This helps improve future search accuracy." }, null, 2),
|
|
545
|
+
}],
|
|
546
|
+
};
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// --- MCP Server ---
|
|
550
|
+
function startServer() {
|
|
551
|
+
// Validate env before starting
|
|
552
|
+
getEnvKeys();
|
|
553
|
+
const useHttp = args.includes("--http") || !!process.env.MCP_HTTP_PORT;
|
|
554
|
+
const port = parseInt(process.env.MCP_HTTP_PORT || process.env.PORT || "3100", 10);
|
|
555
|
+
if (useHttp) {
|
|
556
|
+
const transports = new Map();
|
|
557
|
+
const httpServer = createServer(async (req, res) => {
|
|
558
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
559
|
+
// Health check
|
|
560
|
+
if (url.pathname === "/health") {
|
|
561
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
562
|
+
res.end(JSON.stringify({ status: "ok", version: VERSION }));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (url.pathname === "/mcp") {
|
|
566
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
567
|
+
if (sessionId && transports.has(sessionId)) {
|
|
568
|
+
const transport = transports.get(sessionId);
|
|
569
|
+
await transport.handleRequest(req, res);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// New session
|
|
573
|
+
const transport = new StreamableHTTPServerTransport({
|
|
574
|
+
sessionIdGenerator: () => randomUUID(),
|
|
575
|
+
});
|
|
576
|
+
transport.onclose = () => {
|
|
577
|
+
if (transport.sessionId) {
|
|
578
|
+
transports.delete(transport.sessionId);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const server = new McpServer({
|
|
582
|
+
name: "browseai-dev",
|
|
583
|
+
version: VERSION,
|
|
584
|
+
});
|
|
585
|
+
registerTools(server);
|
|
586
|
+
await server.connect(transport);
|
|
587
|
+
if (transport.sessionId) {
|
|
588
|
+
transports.set(transport.sessionId, transport);
|
|
589
|
+
}
|
|
590
|
+
await transport.handleRequest(req, res);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
res.writeHead(404);
|
|
594
|
+
res.end("Not found");
|
|
595
|
+
});
|
|
596
|
+
httpServer.listen(port, () => {
|
|
597
|
+
console.error(`browseai-dev v${VERSION} MCP server running on http://localhost:${port}/mcp`);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const server = new McpServer({
|
|
602
|
+
name: "browseai-dev",
|
|
603
|
+
version: VERSION,
|
|
604
|
+
});
|
|
605
|
+
registerTools(server);
|
|
606
|
+
async function run() {
|
|
607
|
+
const transport = new StdioServerTransport();
|
|
608
|
+
await server.connect(transport);
|
|
609
|
+
console.error(`browseai-dev v${VERSION} MCP server running on stdio`);
|
|
610
|
+
}
|
|
611
|
+
run().catch((err) => {
|
|
612
|
+
console.error("Failed to start browseai-dev:", err);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSetup(): Promise<void>;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
function ask(question) {
|
|
6
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
7
|
+
}
|
|
8
|
+
function getConfigPath() {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
11
|
+
if (platform === "darwin") {
|
|
12
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
13
|
+
}
|
|
14
|
+
else if (platform === "win32") {
|
|
15
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
return join(home, ".config", "claude", "claude_desktop_config.json");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function runSetup() {
|
|
22
|
+
console.log(`
|
|
23
|
+
browseai-dev setup
|
|
24
|
+
================
|
|
25
|
+
Configure browseai-dev for Claude Desktop / Cursor / Windsurf
|
|
26
|
+
`);
|
|
27
|
+
const browseKey = await ask(" BrowseAI Dev API key (leave blank to use your own Tavily + OpenRouter keys):");
|
|
28
|
+
let mcpEnv;
|
|
29
|
+
if (browseKey.trim()) {
|
|
30
|
+
mcpEnv = { BROWSE_API_KEY: browseKey.trim() };
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const serpKey = await ask(" Tavily API key (get one at https://tavily.com): ");
|
|
34
|
+
if (!serpKey.trim()) {
|
|
35
|
+
console.log("\n Tavily API key is required. Get one at https://tavily.com\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const openrouterKey = await ask(" OpenRouter API key (get one at https://openrouter.ai): ");
|
|
39
|
+
if (!openrouterKey.trim()) {
|
|
40
|
+
console.log("\n OpenRouter API key is required. Get one at https://openrouter.ai\n");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
mcpEnv = {
|
|
44
|
+
SERP_API_KEY: serpKey.trim(),
|
|
45
|
+
OPENROUTER_API_KEY: openrouterKey.trim(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
rl.close();
|
|
49
|
+
const mcpEntry = {
|
|
50
|
+
command: "npx",
|
|
51
|
+
args: ["-y", "browseai-dev"],
|
|
52
|
+
env: mcpEnv,
|
|
53
|
+
};
|
|
54
|
+
const configPath = getConfigPath();
|
|
55
|
+
console.log(`\n Config path: ${configPath}`);
|
|
56
|
+
let config = { mcpServers: {} };
|
|
57
|
+
if (existsSync(configPath)) {
|
|
58
|
+
try {
|
|
59
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
60
|
+
if (!config.mcpServers)
|
|
61
|
+
config.mcpServers = {};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log(" Could not parse existing config, creating new one...");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const dir = configPath.replace(/[/\\][^/\\]+$/, "");
|
|
69
|
+
mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
config.mcpServers["browseai-dev"] = mcpEntry;
|
|
72
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
73
|
+
console.log(`
|
|
74
|
+
Done! browseai-dev has been configured.
|
|
75
|
+
|
|
76
|
+
Next steps:
|
|
77
|
+
1. Restart Claude Desktop
|
|
78
|
+
2. You should see "browseai-dev" in the MCP tools list
|
|
79
|
+
3. Try asking: "Use browse_answer to explain quantum computing"
|
|
80
|
+
|
|
81
|
+
Available tools:
|
|
82
|
+
browse_search - Search the web
|
|
83
|
+
browse_open - Fetch and parse a page
|
|
84
|
+
browse_extract - Extract knowledge from a page
|
|
85
|
+
browse_answer - Full deep research pipeline
|
|
86
|
+
browse_compare - Compare raw LLM vs evidence-backed answer
|
|
87
|
+
|
|
88
|
+
Config written to: ${configPath}
|
|
89
|
+
`);
|
|
90
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browseai-dev",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Reliable research infrastructure for AI agents. MCP server with real-time web search, evidence extraction, and structured citations. The research layer for LangChain, CrewAI, and custom agents.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"claude",
|
|
9
|
+
"ai-agent",
|
|
10
|
+
"web-search",
|
|
11
|
+
"deep-research",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"cursor",
|
|
14
|
+
"windsurf",
|
|
15
|
+
"langchain",
|
|
16
|
+
"crewai",
|
|
17
|
+
"openai",
|
|
18
|
+
"deep-search",
|
|
19
|
+
"rag",
|
|
20
|
+
"agent-tools",
|
|
21
|
+
"research-agent",
|
|
22
|
+
"web-browsing",
|
|
23
|
+
"citations",
|
|
24
|
+
"ai-search",
|
|
25
|
+
"llm-tools",
|
|
26
|
+
"autogpt",
|
|
27
|
+
"llamaindex",
|
|
28
|
+
"evidence",
|
|
29
|
+
"fact-checking"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/BrowseAI-HQ/BrowseAI-Dev.git",
|
|
35
|
+
"directory": "apps/mcp"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://browseai.dev",
|
|
38
|
+
"bugs": "https://github.com/BrowseAI-HQ/BrowseAI-Dev/issues",
|
|
39
|
+
"bin": {
|
|
40
|
+
"browseai-dev": "dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev": "tsx src/index.ts",
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"start": "node dist/index.js",
|
|
50
|
+
"prepublishOnly": "tsc"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
54
|
+
"@mozilla/readability": "^0.6.0",
|
|
55
|
+
"linkedom": "^0.18.0",
|
|
56
|
+
"zod": "^4.3.6"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"tsx": "^4.19.0",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"@types/node": "^25.5.0"
|
|
62
|
+
}
|
|
63
|
+
}
|