web-agent-bridge 1.1.0 → 1.1.1
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/examples/mcp-agent.js +15 -6
- package/package.json +1 -1
- package/script/ai-agent-bridge.js +2 -2
- package/server/index.js +2 -0
- package/server/routes/discovery.js +12 -7
- package/server/routes/noscript.js +1 -1
- package/server/routes/wab-api.js +476 -0
- package/server/services/fairness.js +1 -1
- package/wab-mcp-adapter/README.md +1 -1
- package/wab-mcp-adapter/index.js +43 -16
- package/wab-mcp-adapter/package.json +1 -1
package/examples/mcp-agent.js
CHANGED
|
@@ -50,21 +50,30 @@ async function main() {
|
|
|
50
50
|
// Step 3: Execute built-in tools
|
|
51
51
|
console.log('\n3. Executing wab_get_page_info...');
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const infoResult = await adapter.executeTool('wab_get_page_info', {});
|
|
54
|
+
const info = infoResult.content;
|
|
55
|
+
if (infoResult.is_error) {
|
|
56
|
+
console.log(` Error: ${info.error}`);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(` Title: ${info.title || 'N/A'}`);
|
|
59
|
+
console.log(` Version: ${info.bridgeVersion || 'N/A'}`);
|
|
60
|
+
console.log(` Domain: ${info.domain || 'N/A'}`);
|
|
61
|
+
}
|
|
56
62
|
} catch (err) {
|
|
57
|
-
console.log(`
|
|
63
|
+
console.log(` Error: ${err.message}`);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// Step 4: Search the fairness registry
|
|
61
67
|
console.log('\n4. Fairness-weighted search (demo):');
|
|
62
68
|
try {
|
|
63
|
-
const
|
|
69
|
+
const searchResult = await adapter.executeTool('wab_fairness_search', {
|
|
64
70
|
query: 'e-commerce',
|
|
65
71
|
limit: 5
|
|
66
72
|
});
|
|
67
|
-
|
|
73
|
+
const search = searchResult.content;
|
|
74
|
+
if (searchResult.is_error) {
|
|
75
|
+
console.log(` Error: ${search.error}`);
|
|
76
|
+
} else if (search.results?.length) {
|
|
68
77
|
search.results.forEach(r => {
|
|
69
78
|
console.log(` - ${r.name} (${r.domain}) — score: ${r.final_score}`);
|
|
70
79
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-agent-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Open protocol and runtime for AI agents to interact with websites — standardized discovery, commands, and fairness layer for the Agentic Web",
|
|
5
5
|
"main": "server/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web Agent Bridge v1.1.
|
|
2
|
+
* Web Agent Bridge v1.1.1
|
|
3
3
|
* Open protocol + runtime for AI agent ↔ website interaction
|
|
4
4
|
* https://github.com/web-agent-bridge
|
|
5
5
|
* License: MIT
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
(function (global) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
-
const VERSION = '1.1.
|
|
10
|
+
const VERSION = '1.1.1';
|
|
11
11
|
const PROTOCOL_VERSION = '1.0';
|
|
12
12
|
const LICENSING_SERVER = 'https://api.webagentbridge.com';
|
|
13
13
|
const DISCOVERY_PATHS = ['/agent-bridge.json', '/.well-known/wab.json'];
|
package/server/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const adminRoutes = require('./routes/admin');
|
|
|
20
20
|
const billingRoutes = require('./routes/billing');
|
|
21
21
|
const noscriptRoutes = require('./routes/noscript');
|
|
22
22
|
const discoveryRoutes = require('./routes/discovery');
|
|
23
|
+
const wabApiRoutes = require('./routes/wab-api');
|
|
23
24
|
const { handleWebhookRequest } = require('./services/stripe');
|
|
24
25
|
|
|
25
26
|
const app = express();
|
|
@@ -114,6 +115,7 @@ app.use('/api/license', licenseLimiter, licenseRoutes);
|
|
|
114
115
|
app.use('/api/admin', apiLimiter, adminRoutes);
|
|
115
116
|
app.use('/api/billing', apiLimiter, billingRoutes);
|
|
116
117
|
app.use('/api/noscript', noscriptRoutes);
|
|
118
|
+
app.use('/api/wab', wabApiRoutes);
|
|
117
119
|
app.use('/', discoveryRoutes);
|
|
118
120
|
|
|
119
121
|
app.get('/dashboard', (req, res) => {
|
|
@@ -15,7 +15,7 @@ const {
|
|
|
15
15
|
generateFairnessReport
|
|
16
16
|
} = require('../services/fairness');
|
|
17
17
|
|
|
18
|
-
const WAB_VERSION = '1.1.
|
|
18
|
+
const WAB_VERSION = '1.1.1';
|
|
19
19
|
|
|
20
20
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
21
21
|
|
|
@@ -77,10 +77,10 @@ function buildDiscoveryDocument(site) {
|
|
|
77
77
|
},
|
|
78
78
|
agent_access: {
|
|
79
79
|
bridge_script: '/script/ai-agent-bridge.js',
|
|
80
|
-
api_base: '/api/
|
|
80
|
+
api_base: '/api/wab',
|
|
81
81
|
websocket: '/ws/analytics',
|
|
82
|
-
noscript:
|
|
83
|
-
discovery:
|
|
82
|
+
noscript: `/api/noscript/bridge/${site.id}`,
|
|
83
|
+
discovery: `/api/discovery/${site.id}`
|
|
84
84
|
},
|
|
85
85
|
fairness: {
|
|
86
86
|
is_independent: dirEntry ? !!dirEntry.is_independent : false,
|
|
@@ -95,10 +95,15 @@ function buildDiscoveryDocument(site) {
|
|
|
95
95
|
sandbox: true
|
|
96
96
|
},
|
|
97
97
|
endpoints: {
|
|
98
|
+
authenticate: '/api/wab/authenticate',
|
|
99
|
+
discover: `/api/wab/discover?siteId=${site.id}`,
|
|
100
|
+
actions: `/api/wab/actions?siteId=${site.id}`,
|
|
101
|
+
execute: '/api/wab/actions/{actionName}',
|
|
102
|
+
read: '/api/wab/read',
|
|
103
|
+
page_info: `/api/wab/page-info?siteId=${site.id}`,
|
|
104
|
+
search: '/api/wab/search',
|
|
105
|
+
ping: '/api/wab/ping',
|
|
98
106
|
token_exchange: '/api/license/token',
|
|
99
|
-
verify: '/api/license/verify',
|
|
100
|
-
track: '/api/license/track',
|
|
101
|
-
actions: `/api/discovery/${site.id}`,
|
|
102
107
|
bridge_page: `/api/noscript/bridge/${site.id}`
|
|
103
108
|
}
|
|
104
109
|
};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB Protocol HTTP Transport — RESTful endpoints that implement the
|
|
3
|
+
* WAB command protocol over HTTP for remote agents and the MCP adapter.
|
|
4
|
+
*
|
|
5
|
+
* Every command from the WAB spec (docs/SPEC.md §5) is accessible here
|
|
6
|
+
* so agents that cannot run JavaScript in a browser can still interact
|
|
7
|
+
* with WAB-enabled sites via standard HTTP requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const express = require('express');
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
const { findSiteById, findSiteByLicense, recordAnalytic, db } = require('../models/db');
|
|
13
|
+
const { broadcastAnalytic } = require('../ws');
|
|
14
|
+
const {
|
|
15
|
+
calculateNeutralityScore,
|
|
16
|
+
fairnessWeightedSearch,
|
|
17
|
+
getDirectoryListings,
|
|
18
|
+
generateFairnessReport
|
|
19
|
+
} = require('../services/fairness');
|
|
20
|
+
|
|
21
|
+
const WAB_VERSION = '1.1.1';
|
|
22
|
+
const PROTOCOL_VERSION = '1.0';
|
|
23
|
+
|
|
24
|
+
// ─── Session management ──────────────────────────────────────────────
|
|
25
|
+
const sessions = new Map();
|
|
26
|
+
const SESSION_TTL = 3600_000;
|
|
27
|
+
|
|
28
|
+
setInterval(() => {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [token, data] of sessions) {
|
|
31
|
+
if (now > data.expiresAt) sessions.delete(token);
|
|
32
|
+
}
|
|
33
|
+
}, 300_000);
|
|
34
|
+
|
|
35
|
+
function generateSessionToken() {
|
|
36
|
+
const bytes = require('crypto').randomBytes(32);
|
|
37
|
+
return bytes.toString('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireSession(req, res, next) {
|
|
41
|
+
const auth = req.get('Authorization');
|
|
42
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
43
|
+
return res.status(401).json({
|
|
44
|
+
type: 'error',
|
|
45
|
+
error: { code: 'auth_required', message: 'Bearer token required in Authorization header' }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const token = auth.slice(7);
|
|
49
|
+
const session = sessions.get(token);
|
|
50
|
+
if (!session || Date.now() > session.expiresAt) {
|
|
51
|
+
sessions.delete(token);
|
|
52
|
+
return res.status(401).json({
|
|
53
|
+
type: 'error',
|
|
54
|
+
error: { code: 'session_expired', message: 'Session expired or invalid' }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
req.wabSession = session;
|
|
58
|
+
next();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Helper: resolve site from request ───────────────────────────────
|
|
62
|
+
function resolveSite(req) {
|
|
63
|
+
if (req.wabSession) return findSiteById.get(req.wabSession.siteId);
|
|
64
|
+
const siteId = req.query.siteId || req.body?.siteId;
|
|
65
|
+
if (siteId) return findSiteById.get(siteId);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseSiteConfig(site) {
|
|
70
|
+
try { return JSON.parse(site.config || '{}'); } catch (_) { return {}; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildCommandResponse(id, result) {
|
|
74
|
+
return { id: id || null, type: 'success', protocol: PROTOCOL_VERSION, result };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildErrorResponse(id, code, message) {
|
|
78
|
+
return { id: id || null, type: 'error', protocol: PROTOCOL_VERSION, error: { code, message } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
82
|
+
// POST /api/wab/authenticate — session token exchange
|
|
83
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
router.post('/authenticate', (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { siteId, apiKey, meta } = req.body;
|
|
88
|
+
if (!siteId && !apiKey) {
|
|
89
|
+
return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId or apiKey required'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let site;
|
|
93
|
+
if (apiKey) {
|
|
94
|
+
site = db.prepare('SELECT * FROM sites WHERE api_key = ? AND active = 1').get(apiKey);
|
|
95
|
+
} else {
|
|
96
|
+
site = findSiteById.get(siteId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!site) {
|
|
100
|
+
return res.status(404).json(buildErrorResponse(null, 'not_found', 'Site not found or invalid credentials'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const origin = req.get('origin') || '';
|
|
104
|
+
if (origin) {
|
|
105
|
+
try {
|
|
106
|
+
const reqDomain = new URL(origin).hostname.replace(/^www\./, '');
|
|
107
|
+
const siteDomain = site.domain.replace(/^www\./, '');
|
|
108
|
+
if (reqDomain !== siteDomain && reqDomain !== 'localhost' && reqDomain !== '127.0.0.1') {
|
|
109
|
+
return res.status(403).json(buildErrorResponse(null, 'origin_mismatch', 'Origin does not match site domain'));
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const token = generateSessionToken();
|
|
115
|
+
sessions.set(token, {
|
|
116
|
+
siteId: site.id,
|
|
117
|
+
tier: site.tier,
|
|
118
|
+
domain: site.domain,
|
|
119
|
+
agentMeta: meta || {},
|
|
120
|
+
createdAt: Date.now(),
|
|
121
|
+
expiresAt: Date.now() + SESSION_TTL
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
res.json(buildCommandResponse(null, {
|
|
125
|
+
authenticated: true,
|
|
126
|
+
token,
|
|
127
|
+
siteId: site.id,
|
|
128
|
+
tier: site.tier,
|
|
129
|
+
expiresIn: SESSION_TTL / 1000,
|
|
130
|
+
permissions: parseSiteConfig(site).agentPermissions || {}
|
|
131
|
+
}));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Authentication failed'));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
138
|
+
// GET /api/wab/discover — full discovery document
|
|
139
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
router.get('/discover', (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const site = resolveSite(req);
|
|
144
|
+
if (!site || !site.active) {
|
|
145
|
+
const domain = (req.get('origin') ? new URL(req.get('origin')).hostname : req.get('host')?.split(':')[0]) || '';
|
|
146
|
+
const byDomain = db.prepare(
|
|
147
|
+
'SELECT * FROM sites WHERE LOWER(REPLACE(domain, "www.", "")) = ? AND active = 1 LIMIT 1'
|
|
148
|
+
).get(domain.toLowerCase().replace(/^www\./, ''));
|
|
149
|
+
|
|
150
|
+
if (!byDomain) {
|
|
151
|
+
return res.status(404).json(buildErrorResponse(null, 'not_found', 'No WAB site found'));
|
|
152
|
+
}
|
|
153
|
+
return res.json(buildCommandResponse(null, buildDiscovery(byDomain)));
|
|
154
|
+
}
|
|
155
|
+
res.json(buildCommandResponse(null, buildDiscovery(site)));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Discovery failed'));
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
162
|
+
// GET /api/wab/actions — list actions
|
|
163
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
router.get('/actions', (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const site = resolveSite(req);
|
|
168
|
+
if (!site) return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId required'));
|
|
169
|
+
|
|
170
|
+
const config = parseSiteConfig(site);
|
|
171
|
+
const perms = config.agentPermissions || {};
|
|
172
|
+
const category = req.query.category;
|
|
173
|
+
|
|
174
|
+
const actions = Object.entries(perms)
|
|
175
|
+
.filter(([, v]) => v)
|
|
176
|
+
.map(([name]) => ({
|
|
177
|
+
name,
|
|
178
|
+
description: `Permission: ${name}`,
|
|
179
|
+
trigger: name === 'click' ? 'click' : name === 'fillForms' ? 'fill_and_submit' : name === 'scroll' ? 'scroll' : 'api',
|
|
180
|
+
category: name === 'navigate' ? 'navigation' : 'general',
|
|
181
|
+
requiresAuth: ['apiAccess', 'automatedLogin', 'extractData'].includes(name)
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const filtered = category ? actions.filter(a => a.category === category) : actions;
|
|
185
|
+
|
|
186
|
+
res.json(buildCommandResponse(req.query.id || null, { actions: filtered, total: filtered.length }));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Failed to list actions'));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
193
|
+
// POST /api/wab/actions/:name — execute action (with tracking)
|
|
194
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
195
|
+
|
|
196
|
+
router.post('/actions/:name', requireSession, (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const actionName = req.params.name;
|
|
199
|
+
const site = findSiteById.get(req.wabSession.siteId);
|
|
200
|
+
if (!site) return res.status(404).json(buildErrorResponse(req.body?.id, 'not_found', 'Site not found'));
|
|
201
|
+
|
|
202
|
+
const config = parseSiteConfig(site);
|
|
203
|
+
const perms = config.agentPermissions || {};
|
|
204
|
+
|
|
205
|
+
const permMap = {
|
|
206
|
+
click: 'click', fill_and_submit: 'fillForms', scroll: 'scroll',
|
|
207
|
+
navigate: 'navigate', api: 'apiAccess', read: 'readContent', extract: 'extractData'
|
|
208
|
+
};
|
|
209
|
+
const requiredPerm = permMap[actionName] || actionName;
|
|
210
|
+
|
|
211
|
+
if (!perms[requiredPerm] && !perms[actionName]) {
|
|
212
|
+
return res.status(403).json(buildErrorResponse(req.body?.id, 'permission_denied',
|
|
213
|
+
`Action "${actionName}" is not permitted by site configuration`));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
recordAnalytic({
|
|
217
|
+
siteId: site.id,
|
|
218
|
+
actionName,
|
|
219
|
+
agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
|
|
220
|
+
triggerType: 'wab_api',
|
|
221
|
+
success: true,
|
|
222
|
+
metadata: { params: req.body?.params || {}, transport: 'http' }
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
broadcastAnalytic(site.id, {
|
|
226
|
+
actionName,
|
|
227
|
+
agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
|
|
228
|
+
triggerType: 'wab_api',
|
|
229
|
+
success: true
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
res.json(buildCommandResponse(req.body?.id, {
|
|
233
|
+
success: true,
|
|
234
|
+
action: actionName,
|
|
235
|
+
siteId: site.id,
|
|
236
|
+
executed_at: new Date().toISOString(),
|
|
237
|
+
note: 'Server-side action recorded. For DOM interactions, use the bridge script in-browser.'
|
|
238
|
+
}));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
res.status(500).json(buildErrorResponse(req.body?.id, 'internal', 'Action execution failed'));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
245
|
+
// POST /api/wab/read — read content (selector-based, requires in-browser)
|
|
246
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
247
|
+
|
|
248
|
+
router.post('/read', requireSession, (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
const { selector, id } = req.body;
|
|
251
|
+
if (!selector) {
|
|
252
|
+
return res.status(400).json(buildErrorResponse(id, 'invalid_argument', 'selector is required'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const site = findSiteById.get(req.wabSession.siteId);
|
|
256
|
+
if (!site) return res.status(404).json(buildErrorResponse(id, 'not_found', 'Site not found'));
|
|
257
|
+
|
|
258
|
+
const config = parseSiteConfig(site);
|
|
259
|
+
if (!config.agentPermissions?.readContent) {
|
|
260
|
+
return res.status(403).json(buildErrorResponse(id, 'permission_denied', 'readContent not enabled'));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
recordAnalytic({
|
|
264
|
+
siteId: site.id,
|
|
265
|
+
actionName: 'readContent',
|
|
266
|
+
agentId: req.wabSession.agentMeta?.name || 'mcp-agent',
|
|
267
|
+
triggerType: 'wab_api',
|
|
268
|
+
success: true,
|
|
269
|
+
metadata: { selector, transport: 'http' }
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
res.json(buildCommandResponse(id, {
|
|
273
|
+
success: true,
|
|
274
|
+
selector,
|
|
275
|
+
note: 'Content reading via HTTP returns metadata only. Use the bridge script in-browser or the noscript bridge for rendered content.',
|
|
276
|
+
bridge_page: `/api/noscript/bridge/${site.id}`,
|
|
277
|
+
noscript_endpoints: {
|
|
278
|
+
pixel: `/api/noscript/pixel/${site.id}`,
|
|
279
|
+
css: `/api/noscript/css/${site.id}`,
|
|
280
|
+
bridge: `/api/noscript/bridge/${site.id}`
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
} catch (err) {
|
|
284
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Read failed'));
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
289
|
+
// GET /api/wab/page-info — get page/site metadata
|
|
290
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
291
|
+
|
|
292
|
+
router.get('/page-info', (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const site = resolveSite(req);
|
|
295
|
+
if (!site) return res.status(400).json(buildErrorResponse(null, 'invalid_argument', 'siteId required'));
|
|
296
|
+
|
|
297
|
+
const config = parseSiteConfig(site);
|
|
298
|
+
const neutralityScore = calculateNeutralityScore(site);
|
|
299
|
+
|
|
300
|
+
res.json(buildCommandResponse(req.query.id || null, {
|
|
301
|
+
title: site.name,
|
|
302
|
+
domain: site.domain,
|
|
303
|
+
url: `https://${site.domain}`,
|
|
304
|
+
tier: site.tier,
|
|
305
|
+
bridgeVersion: WAB_VERSION,
|
|
306
|
+
protocol: PROTOCOL_VERSION,
|
|
307
|
+
permissions: config.agentPermissions || {},
|
|
308
|
+
restrictions: config.restrictions || {},
|
|
309
|
+
security: {
|
|
310
|
+
sandboxActive: true,
|
|
311
|
+
sessionRequired: true,
|
|
312
|
+
originValidation: true,
|
|
313
|
+
rateLimit: config.restrictions?.rateLimit?.maxCallsPerMinute || 60
|
|
314
|
+
},
|
|
315
|
+
fairness: {
|
|
316
|
+
neutralityScore,
|
|
317
|
+
isIndependent: false
|
|
318
|
+
},
|
|
319
|
+
endpoints: {
|
|
320
|
+
discover: `/api/wab/discover?siteId=${site.id}`,
|
|
321
|
+
actions: `/api/wab/actions?siteId=${site.id}`,
|
|
322
|
+
authenticate: '/api/wab/authenticate',
|
|
323
|
+
bridge: `/api/noscript/bridge/${site.id}`,
|
|
324
|
+
discovery: `/api/discovery/${site.id}`
|
|
325
|
+
}
|
|
326
|
+
}));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Failed to get page info'));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
333
|
+
// GET /api/wab/search — fairness-weighted search (MCP adapter uses this)
|
|
334
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
335
|
+
|
|
336
|
+
router.get('/search', (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const query = req.query.q || '';
|
|
339
|
+
const category = req.query.category || null;
|
|
340
|
+
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
|
|
341
|
+
|
|
342
|
+
let sql = `
|
|
343
|
+
SELECT s.*, d.category, d.tags, d.is_independent, d.commission_rate,
|
|
344
|
+
d.direct_benefit, d.neutrality_score, d.trust_signature
|
|
345
|
+
FROM wab_directory d
|
|
346
|
+
JOIN sites s ON d.site_id = s.id AND s.active = 1
|
|
347
|
+
WHERE d.listed = 1
|
|
348
|
+
`;
|
|
349
|
+
const params = [];
|
|
350
|
+
|
|
351
|
+
if (category) {
|
|
352
|
+
sql += ' AND d.category = ?';
|
|
353
|
+
params.push(category);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
sql += ' ORDER BY d.neutrality_score DESC LIMIT ?';
|
|
357
|
+
params.push(limit * 3);
|
|
358
|
+
|
|
359
|
+
const candidates = db.prepare(sql).all(...params);
|
|
360
|
+
const results = fairnessWeightedSearch(query, candidates).slice(0, limit);
|
|
361
|
+
|
|
362
|
+
res.json(buildCommandResponse(req.query.id || null, {
|
|
363
|
+
query,
|
|
364
|
+
total: results.length,
|
|
365
|
+
fairness_applied: true,
|
|
366
|
+
results: results.map(r => ({
|
|
367
|
+
siteId: r.id,
|
|
368
|
+
name: r.name,
|
|
369
|
+
domain: r.domain,
|
|
370
|
+
description: r.description || '',
|
|
371
|
+
category: r.category || 'general',
|
|
372
|
+
tier: r.tier,
|
|
373
|
+
neutrality_score: r._neutralityScore,
|
|
374
|
+
is_independent: r._isIndependent,
|
|
375
|
+
relevance_score: r._relevance,
|
|
376
|
+
fairness_boost: r._fairnessBoost,
|
|
377
|
+
final_score: r._finalScore,
|
|
378
|
+
endpoints: {
|
|
379
|
+
discover: `/api/wab/discover?siteId=${r.id}`,
|
|
380
|
+
actions: `/api/wab/actions?siteId=${r.id}`,
|
|
381
|
+
bridge: `/api/noscript/bridge/${r.id}`
|
|
382
|
+
}
|
|
383
|
+
}))
|
|
384
|
+
}));
|
|
385
|
+
} catch (err) {
|
|
386
|
+
res.status(500).json(buildErrorResponse(null, 'internal', 'Search failed'));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
391
|
+
// GET /api/wab/ping — health check
|
|
392
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
393
|
+
|
|
394
|
+
router.get('/ping', (_req, res) => {
|
|
395
|
+
res.json(buildCommandResponse(null, {
|
|
396
|
+
pong: true,
|
|
397
|
+
version: WAB_VERSION,
|
|
398
|
+
protocol: PROTOCOL_VERSION,
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
status: 'healthy'
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ─── Discovery document builder ──────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
function buildDiscovery(site) {
|
|
407
|
+
const config = parseSiteConfig(site);
|
|
408
|
+
const perms = config.agentPermissions || {};
|
|
409
|
+
const features = config.features || {};
|
|
410
|
+
|
|
411
|
+
const commands = Object.entries(perms)
|
|
412
|
+
.filter(([, v]) => v)
|
|
413
|
+
.map(([name]) => ({
|
|
414
|
+
name,
|
|
415
|
+
trigger: name === 'click' ? 'click' : name === 'fillForms' ? 'fill_and_submit' : name === 'scroll' ? 'scroll' : 'api',
|
|
416
|
+
requiresAuth: ['apiAccess', 'automatedLogin', 'extractData'].includes(name)
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
const featureList = ['auto_discovery', 'noscript_fallback', 'wab_protocol_api'];
|
|
420
|
+
if (features.advancedAnalytics) featureList.push('advanced_analytics');
|
|
421
|
+
if (features.realTimeUpdates) featureList.push('real_time_updates');
|
|
422
|
+
|
|
423
|
+
const dirEntry = db.prepare('SELECT * FROM wab_directory WHERE site_id = ?').get(site.id);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
wab_version: WAB_VERSION,
|
|
427
|
+
protocol: PROTOCOL_VERSION,
|
|
428
|
+
generated_at: new Date().toISOString(),
|
|
429
|
+
provider: {
|
|
430
|
+
name: site.name,
|
|
431
|
+
domain: site.domain,
|
|
432
|
+
category: dirEntry?.category || 'general',
|
|
433
|
+
description: site.description || ''
|
|
434
|
+
},
|
|
435
|
+
capabilities: {
|
|
436
|
+
commands,
|
|
437
|
+
permissions: perms,
|
|
438
|
+
tier: site.tier,
|
|
439
|
+
transport: ['js_global', 'http', 'websocket'],
|
|
440
|
+
features: featureList
|
|
441
|
+
},
|
|
442
|
+
agent_access: {
|
|
443
|
+
bridge_script: '/script/ai-agent-bridge.js',
|
|
444
|
+
api_base: '/api/wab',
|
|
445
|
+
websocket: '/ws/analytics',
|
|
446
|
+
noscript: `/api/noscript/bridge/${site.id}`,
|
|
447
|
+
discovery: `/api/discovery/${site.id}`
|
|
448
|
+
},
|
|
449
|
+
fairness: {
|
|
450
|
+
is_independent: dirEntry ? !!dirEntry.is_independent : false,
|
|
451
|
+
commission_rate: dirEntry ? dirEntry.commission_rate : 0,
|
|
452
|
+
direct_benefit: dirEntry ? (dirEntry.direct_benefit || '') : '',
|
|
453
|
+
neutrality_score: calculateNeutralityScore(site)
|
|
454
|
+
},
|
|
455
|
+
security: {
|
|
456
|
+
session_required: true,
|
|
457
|
+
origin_validation: true,
|
|
458
|
+
rate_limit: config.restrictions?.rateLimit?.maxCallsPerMinute || 60,
|
|
459
|
+
sandbox: true
|
|
460
|
+
},
|
|
461
|
+
endpoints: {
|
|
462
|
+
authenticate: '/api/wab/authenticate',
|
|
463
|
+
discover: `/api/wab/discover?siteId=${site.id}`,
|
|
464
|
+
actions: `/api/wab/actions?siteId=${site.id}`,
|
|
465
|
+
execute: '/api/wab/actions/{actionName}',
|
|
466
|
+
read: '/api/wab/read',
|
|
467
|
+
page_info: `/api/wab/page-info?siteId=${site.id}`,
|
|
468
|
+
search: '/api/wab/search',
|
|
469
|
+
ping: '/api/wab/ping',
|
|
470
|
+
token_exchange: '/api/license/token',
|
|
471
|
+
bridge_page: `/api/noscript/bridge/${site.id}`
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = router;
|
|
@@ -39,7 +39,7 @@ adapter.close();
|
|
|
39
39
|
| `siteId` | `string` | `null` | WAB site identifier |
|
|
40
40
|
| `apiKey` | `string` | `null` | API key for authenticated requests |
|
|
41
41
|
| `transport` | `string` | `'http'` | Transport type: `http`, `websocket`, or `direct` |
|
|
42
|
-
| `registryUrl` | `string` | `https://
|
|
42
|
+
| `registryUrl` | `string` | `https://webagentbridge.com` | WAB fairness registry URL |
|
|
43
43
|
| `page` | `object` | — | Puppeteer/Playwright page (required for `direct`) |
|
|
44
44
|
| `wsUrl` | `string` | auto | WebSocket URL (required for `websocket` if no `siteUrl`) |
|
|
45
45
|
| `timeout` | `number` | `15000` | Request timeout in milliseconds |
|
package/wab-mcp-adapter/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
13
|
const DISCOVERY_PATHS = ['/agent-bridge.json', '/.well-known/wab.json'];
|
|
14
|
-
const DEFAULT_REGISTRY = 'https://
|
|
14
|
+
const DEFAULT_REGISTRY = 'https://webagentbridge.com';
|
|
15
15
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -320,20 +320,35 @@ class WABMCPAdapter {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
if (this.siteId) {
|
|
324
|
+
try {
|
|
325
|
+
this._discovery = await jsonFetch(
|
|
326
|
+
resolveUrl(base, `/api/discovery/${this.siteId}`), {}, this.timeout
|
|
327
|
+
);
|
|
328
|
+
this._extractActions(this._discovery);
|
|
329
|
+
return this._discovery;
|
|
330
|
+
} catch (err) { lastError = err; }
|
|
331
|
+
}
|
|
332
|
+
|
|
323
333
|
try {
|
|
324
|
-
this._discovery = await
|
|
334
|
+
this._discovery = await jsonFetch(
|
|
335
|
+
resolveUrl(base, '/api/wab/discover'), {}, this.timeout
|
|
336
|
+
);
|
|
337
|
+
if (this._discovery.result) this._discovery = this._discovery.result;
|
|
325
338
|
this._extractActions(this._discovery);
|
|
326
339
|
return this._discovery;
|
|
327
|
-
} catch (err) {
|
|
328
|
-
lastError = err;
|
|
329
|
-
}
|
|
340
|
+
} catch (err) { lastError = err; }
|
|
330
341
|
|
|
331
342
|
throw new Error(`WAB discovery failed for ${base}: ${lastError?.message}`);
|
|
332
343
|
}
|
|
333
344
|
|
|
334
345
|
/** @private */
|
|
335
346
|
_extractActions(doc) {
|
|
336
|
-
|
|
347
|
+
const actions = doc.actions || doc.capabilities?.commands || doc.capabilities?.actions || [];
|
|
348
|
+
this._siteActions = Array.isArray(actions) ? actions.map(a => {
|
|
349
|
+
if (typeof a === 'string') return { name: a, description: `Permission: ${a}`, trigger: 'api' };
|
|
350
|
+
return a;
|
|
351
|
+
}) : [];
|
|
337
352
|
}
|
|
338
353
|
|
|
339
354
|
// -----------------------------------------------------------------------
|
|
@@ -430,7 +445,7 @@ class WABMCPAdapter {
|
|
|
430
445
|
method: 'POST',
|
|
431
446
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
432
447
|
body: JSON.stringify({ params: params || {} }),
|
|
433
|
-
}, this.timeout);
|
|
448
|
+
}, this.timeout).then(r => r.result || r);
|
|
434
449
|
}
|
|
435
450
|
|
|
436
451
|
return this._transport.request(`/api/wab/actions/${name}`, { name, data: params || {} });
|
|
@@ -446,7 +461,7 @@ class WABMCPAdapter {
|
|
|
446
461
|
method: 'POST',
|
|
447
462
|
headers: { 'Content-Type': 'application/json', ...this._authHeaders() },
|
|
448
463
|
body: JSON.stringify({ selector }),
|
|
449
|
-
}, this.timeout);
|
|
464
|
+
}, this.timeout).then(r => r.result || r);
|
|
450
465
|
}
|
|
451
466
|
|
|
452
467
|
return this._transport.request('/api/wab/read', { selector });
|
|
@@ -455,7 +470,11 @@ class WABMCPAdapter {
|
|
|
455
470
|
/** @private */
|
|
456
471
|
async _getPageInfo() {
|
|
457
472
|
if (this._transport instanceof HTTPTransport) {
|
|
458
|
-
|
|
473
|
+
const siteParam = this.siteId ? `?siteId=${this.siteId}` : '';
|
|
474
|
+
return jsonFetch(
|
|
475
|
+
resolveUrl(this.siteUrl, `/api/wab/page-info${siteParam}`),
|
|
476
|
+
{ headers: this._authHeaders() }, this.timeout
|
|
477
|
+
).then(r => r.result || r);
|
|
459
478
|
}
|
|
460
479
|
return this._transport.request('/api/wab/page-info');
|
|
461
480
|
}
|
|
@@ -474,10 +493,12 @@ class WABMCPAdapter {
|
|
|
474
493
|
* @returns {Promise<object>}
|
|
475
494
|
*/
|
|
476
495
|
async _fairnessSearch(query, category, limit = 10) {
|
|
477
|
-
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
496
|
+
const params = new URLSearchParams({ q: query || '', limit: String(limit) });
|
|
478
497
|
if (category) params.set('category', category);
|
|
479
498
|
|
|
480
|
-
|
|
499
|
+
const base = this.siteUrl || this.registryUrl;
|
|
500
|
+
const result = await jsonFetch(`${base.replace(/\/+$/, '')}/api/wab/search?${params}`, {}, this.timeout);
|
|
501
|
+
return result.result || result;
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
// -----------------------------------------------------------------------
|
|
@@ -488,7 +509,11 @@ class WABMCPAdapter {
|
|
|
488
509
|
async _authenticate(apiKey, meta) {
|
|
489
510
|
if (!apiKey) throw new Error('apiKey is required');
|
|
490
511
|
|
|
491
|
-
const payload = {
|
|
512
|
+
const payload = {
|
|
513
|
+
apiKey,
|
|
514
|
+
...(this.siteId ? { siteId: this.siteId } : {}),
|
|
515
|
+
...(meta ? { meta } : {})
|
|
516
|
+
};
|
|
492
517
|
|
|
493
518
|
if (this._transport instanceof HTTPTransport) {
|
|
494
519
|
const result = await jsonFetch(resolveUrl(this.siteUrl, '/api/wab/authenticate'), {
|
|
@@ -496,13 +521,15 @@ class WABMCPAdapter {
|
|
|
496
521
|
headers: { 'Content-Type': 'application/json' },
|
|
497
522
|
body: JSON.stringify(payload),
|
|
498
523
|
}, this.timeout);
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
const data = result.result || result;
|
|
525
|
+
if (data.token) this._sessionToken = data.token;
|
|
526
|
+
return data;
|
|
501
527
|
}
|
|
502
528
|
|
|
503
529
|
const result = await this._transport.request('/api/wab/authenticate', payload);
|
|
504
|
-
|
|
505
|
-
|
|
530
|
+
const data = result.result || result;
|
|
531
|
+
if (data.token) this._sessionToken = data.token;
|
|
532
|
+
return data;
|
|
506
533
|
}
|
|
507
534
|
|
|
508
535
|
/** @private Build auth headers from session token and/or API key. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wab-mcp-adapter",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "MCP adapter for Web Agent Bridge — expose WAB site capabilities as MCP tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": ["wab", "mcp", "ai-agent", "model-context-protocol", "web-agent-bridge"],
|