textweb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/docs/index.html +761 -0
- package/mcp/index.js +275 -0
- package/package.json +34 -0
- package/src/apply.js +565 -0
- package/src/browser.js +134 -0
- package/src/cli.js +427 -0
- package/src/renderer.js +452 -0
- package/src/server.js +504 -0
- package/tools/crewai.py +128 -0
- package/tools/langchain.py +165 -0
- package/tools/system_prompt.md +37 -0
- package/tools/tool_definitions.json +154 -0
package/mcp/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TextWeb MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Model Context Protocol server that gives any MCP client
|
|
7
|
+
* (Claude Desktop, Cursor, Windsurf, Cline, OpenClaw, etc.)
|
|
8
|
+
* text-based web browsing capabilities.
|
|
9
|
+
*
|
|
10
|
+
* Communicates over stdio using JSON-RPC 2.0.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { AgentBrowser } = require('../src/browser');
|
|
14
|
+
|
|
15
|
+
const SERVER_INFO = {
|
|
16
|
+
name: 'textweb',
|
|
17
|
+
version: '0.1.0',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: 'textweb_navigate',
|
|
23
|
+
description: 'Navigate to a URL and render the page as a structured text grid. Interactive elements are annotated with [ref] numbers for clicking/typing. Returns the text grid view, element map, and page metadata. Use this as your primary way to view web pages — no screenshots or vision model needed.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
url: { type: 'string', description: 'The URL to navigate to' },
|
|
28
|
+
cols: { type: 'number', description: 'Grid width in characters (default: 120)' },
|
|
29
|
+
},
|
|
30
|
+
required: ['url'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'textweb_click',
|
|
35
|
+
description: 'Click an interactive element by its reference number. Returns the updated text grid after the click (page may navigate or update).',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
ref: { type: 'number', description: 'Element reference number from the text grid (e.g., 3 for [3])' },
|
|
40
|
+
},
|
|
41
|
+
required: ['ref'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'textweb_type',
|
|
46
|
+
description: 'Type text into an input field by its reference number. Clears existing content and types the new text. Returns the updated text grid.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
ref: { type: 'number', description: 'Element reference number of the input field' },
|
|
51
|
+
text: { type: 'string', description: 'Text to type into the field' },
|
|
52
|
+
},
|
|
53
|
+
required: ['ref', 'text'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'textweb_select',
|
|
58
|
+
description: 'Select an option from a dropdown/select element by its reference number.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
ref: { type: 'number', description: 'Element reference number of the select/dropdown' },
|
|
63
|
+
value: { type: 'string', description: 'Value or visible text of the option to select' },
|
|
64
|
+
},
|
|
65
|
+
required: ['ref', 'value'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'textweb_scroll',
|
|
70
|
+
description: 'Scroll the page up or down. Returns the updated text grid showing the new viewport position.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
direction: { type: 'string', enum: ['up', 'down', 'top'], description: 'Scroll direction' },
|
|
75
|
+
amount: { type: 'number', description: 'Number of pages to scroll (default: 1)' },
|
|
76
|
+
},
|
|
77
|
+
required: ['direction'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'textweb_snapshot',
|
|
82
|
+
description: 'Re-render the current page as a text grid without navigating. Useful after waiting for dynamic content to load.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'textweb_press',
|
|
90
|
+
description: 'Press a keyboard key (e.g., Enter, Tab, Escape, ArrowDown). Returns the updated text grid.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
key: { type: 'string', description: 'Key to press (e.g., "Enter", "Tab", "Escape", "ArrowDown")' },
|
|
95
|
+
},
|
|
96
|
+
required: ['key'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'textweb_upload',
|
|
101
|
+
description: 'Upload a file to a file input element by its reference number.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
ref: { type: 'number', description: 'Element reference number of the file input' },
|
|
106
|
+
path: { type: 'string', description: 'Absolute path to the file to upload' },
|
|
107
|
+
},
|
|
108
|
+
required: ['ref', 'path'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// ─── Browser Instance ────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
let browser = null;
|
|
116
|
+
|
|
117
|
+
async function getBrowser(cols) {
|
|
118
|
+
if (!browser) {
|
|
119
|
+
browser = new AgentBrowser({ cols: cols || 120, headless: true });
|
|
120
|
+
await browser.launch();
|
|
121
|
+
}
|
|
122
|
+
return browser;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatResult(result) {
|
|
126
|
+
const refs = Object.entries(result.elements || {})
|
|
127
|
+
.map(([ref, el]) => `[${ref}] ${el.semantic}: ${el.text || '(no text)'}`)
|
|
128
|
+
.join('\n');
|
|
129
|
+
|
|
130
|
+
return `URL: ${result.meta?.url || 'unknown'}\nTitle: ${result.meta?.title || 'unknown'}\nRefs: ${result.meta?.totalRefs || 0}\n\n${result.view}\n\nInteractive elements:\n${refs}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Tool Execution ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function executeTool(name, args) {
|
|
136
|
+
const b = await getBrowser(args.cols);
|
|
137
|
+
|
|
138
|
+
switch (name) {
|
|
139
|
+
case 'textweb_navigate': {
|
|
140
|
+
const result = await b.navigate(args.url);
|
|
141
|
+
return formatResult(result);
|
|
142
|
+
}
|
|
143
|
+
case 'textweb_click': {
|
|
144
|
+
const result = await b.click(args.ref);
|
|
145
|
+
return formatResult(result);
|
|
146
|
+
}
|
|
147
|
+
case 'textweb_type': {
|
|
148
|
+
const result = await b.type(args.ref, args.text);
|
|
149
|
+
return formatResult(result);
|
|
150
|
+
}
|
|
151
|
+
case 'textweb_select': {
|
|
152
|
+
const result = await b.select(args.ref, args.value);
|
|
153
|
+
return formatResult(result);
|
|
154
|
+
}
|
|
155
|
+
case 'textweb_scroll': {
|
|
156
|
+
const result = await b.scroll(args.direction, args.amount || 1);
|
|
157
|
+
return formatResult(result);
|
|
158
|
+
}
|
|
159
|
+
case 'textweb_snapshot': {
|
|
160
|
+
const result = await b.snapshot();
|
|
161
|
+
return formatResult(result);
|
|
162
|
+
}
|
|
163
|
+
case 'textweb_press': {
|
|
164
|
+
const result = await b.press(args.key);
|
|
165
|
+
return formatResult(result);
|
|
166
|
+
}
|
|
167
|
+
case 'textweb_upload': {
|
|
168
|
+
const result = await b.upload(args.ref, args.path);
|
|
169
|
+
return formatResult(result);
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── JSON-RPC / MCP Protocol ────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function jsonrpc(id, result) {
|
|
179
|
+
return JSON.stringify({ jsonrpc: '2.0', id, result });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function jsonrpcError(id, code, message) {
|
|
183
|
+
return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function handleMessage(msg) {
|
|
187
|
+
const { id, method, params } = msg;
|
|
188
|
+
|
|
189
|
+
switch (method) {
|
|
190
|
+
case 'initialize':
|
|
191
|
+
return jsonrpc(id, {
|
|
192
|
+
protocolVersion: '2024-11-05',
|
|
193
|
+
capabilities: { tools: {} },
|
|
194
|
+
serverInfo: SERVER_INFO,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
case 'notifications/initialized':
|
|
198
|
+
return null; // No response needed
|
|
199
|
+
|
|
200
|
+
case 'tools/list':
|
|
201
|
+
return jsonrpc(id, { tools: TOOLS });
|
|
202
|
+
|
|
203
|
+
case 'tools/call': {
|
|
204
|
+
const { name, arguments: args } = params;
|
|
205
|
+
try {
|
|
206
|
+
const text = await executeTool(name, args || {});
|
|
207
|
+
return jsonrpc(id, {
|
|
208
|
+
content: [{ type: 'text', text }],
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return jsonrpc(id, {
|
|
212
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
213
|
+
isError: true,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case 'ping':
|
|
219
|
+
return jsonrpc(id, {});
|
|
220
|
+
|
|
221
|
+
default:
|
|
222
|
+
if (id) return jsonrpcError(id, -32601, `Method not found: ${method}`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── stdio Transport ─────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
function main() {
|
|
230
|
+
let buffer = '';
|
|
231
|
+
|
|
232
|
+
process.stdin.setEncoding('utf8');
|
|
233
|
+
process.stdin.on('data', async (chunk) => {
|
|
234
|
+
buffer += chunk;
|
|
235
|
+
|
|
236
|
+
// Process complete lines (newline-delimited JSON)
|
|
237
|
+
const lines = buffer.split('\n');
|
|
238
|
+
buffer = lines.pop(); // Keep incomplete line in buffer
|
|
239
|
+
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed) continue;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const msg = JSON.parse(trimmed);
|
|
246
|
+
const response = await handleMessage(msg);
|
|
247
|
+
if (response) {
|
|
248
|
+
process.stdout.write(response + '\n');
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// Parse error
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
jsonrpcError(null, -32700, `Parse error: ${err.message}`) + '\n'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
process.stdin.on('end', async () => {
|
|
260
|
+
if (browser) await browser.close();
|
|
261
|
+
process.exit(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
process.on('SIGINT', async () => {
|
|
265
|
+
if (browser) await browser.close();
|
|
266
|
+
process.exit(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
process.on('SIGTERM', async () => {
|
|
270
|
+
if (browser) await browser.close();
|
|
271
|
+
process.exit(0);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "textweb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A text-grid web renderer for AI agents — see the web without screenshots",
|
|
5
|
+
"main": "src/browser.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"textweb": "src/cli.js",
|
|
8
|
+
"textweb-mcp": "mcp/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/cli.js",
|
|
12
|
+
"serve": "node src/server.js",
|
|
13
|
+
"test": "node test/basic.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"browser",
|
|
19
|
+
"text",
|
|
20
|
+
"renderer",
|
|
21
|
+
"llm",
|
|
22
|
+
"headless",
|
|
23
|
+
"accessibility"
|
|
24
|
+
],
|
|
25
|
+
"author": "Christopher Robison <cdr@cdr2.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/chrisrobison/textweb.git"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"playwright": "^1.50.0"
|
|
33
|
+
}
|
|
34
|
+
}
|