@builtwith/sdk 1.0.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.
Files changed (2) hide show
  1. package/package.json +30 -0
  2. package/src/index.js +358 -0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@builtwith/sdk",
3
+ "version": "1.0.0",
4
+ "description": "BuiltWith AI-first SDK for Node.js",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "node test/test.js",
15
+ "example": "node examples/examples.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=22.0.0"
19
+ },
20
+ "keywords": ["builtwith", "sdk", "technology-lookup", "ai", "mcp"],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/BuiltWith/builtwith-ai-sdk.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/BuiltWith/builtwith-ai-sdk/issues"
27
+ },
28
+ "homepage": "https://github.com/BuiltWith/builtwith-ai-sdk#readme",
29
+ "license": "MIT"
30
+ }
package/src/index.js ADDED
@@ -0,0 +1,358 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const { URL } = require('url');
5
+
6
+ // ── Constants ──────────────────────────────────────────────────────────────────
7
+
8
+ const MCP_ENDPOINT = 'https://api.builtwith.com/mcp';
9
+ const MAX_RETRIES = 3;
10
+ const INITIAL_BACKOFF_MS = 1000;
11
+ const DOMAIN_RE = /^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.[a-zA-Z]{2,})+$/;
12
+
13
+ // ── Errors ─────────────────────────────────────────────────────────────────────
14
+
15
+ class BuiltWithError extends Error {
16
+ constructor(error_code, message, http_status, details = null, suggested_fix = null) {
17
+ super(message);
18
+ this.name = 'BuiltWithError';
19
+ this.error_code = error_code;
20
+ this.message = message;
21
+ this.http_status = http_status;
22
+ this.details = details;
23
+ this.suggested_fix = suggested_fix;
24
+ }
25
+
26
+ toJSON() {
27
+ const obj = { error_code: this.error_code, message: this.message, http_status: this.http_status };
28
+ if (this.details != null) obj.details = this.details;
29
+ if (this.suggested_fix != null) obj.suggested_fix = this.suggested_fix;
30
+ return obj;
31
+ }
32
+ }
33
+
34
+ // ── Helpers ────────────────────────────────────────────────────────────────────
35
+
36
+ function _validate_domain(value) {
37
+ if (typeof value !== 'string' || value.length === 0) {
38
+ throw new BuiltWithError('VALIDATION_ERROR', 'Domain is required and must be a non-empty string.', 0, null, 'Provide a root domain like "example.com".');
39
+ }
40
+ if (/^[a-zA-Z][a-zA-Z+\-.]*:\/\//.test(value)) {
41
+ throw new BuiltWithError('VALIDATION_ERROR', `Domain must not include a scheme. Got: "${value}"`, 0, null, 'Remove the scheme (e.g. "https://") and pass only the root domain.');
42
+ }
43
+ if (value.includes('/')) {
44
+ throw new BuiltWithError('VALIDATION_ERROR', `Domain must not include a path. Got: "${value}"`, 0, null, 'Remove any path segments and pass only the root domain.');
45
+ }
46
+ if (value.includes('?') || value.includes('#')) {
47
+ throw new BuiltWithError('VALIDATION_ERROR', `Domain must not include query or fragment. Got: "${value}"`, 0, null, 'Pass only the root domain.');
48
+ }
49
+ if (!DOMAIN_RE.test(value)) {
50
+ throw new BuiltWithError('VALIDATION_ERROR', `Invalid domain format: "${value}"`, 0, null, 'Provide a valid root domain like "example.com".');
51
+ }
52
+ }
53
+
54
+ function _validate_string(name, value) {
55
+ if (typeof value !== 'string' || value.length === 0) {
56
+ throw new BuiltWithError('VALIDATION_ERROR', `${name} is required and must be a non-empty string.`, 0, null, `Provide a valid ${name}.`);
57
+ }
58
+ }
59
+
60
+ function _validate_input(schema_props, required, params) {
61
+ for (const key of required) {
62
+ if (params[key] === undefined || params[key] === null) {
63
+ throw new BuiltWithError('VALIDATION_ERROR', `Missing required parameter: "${key}".`, 0, null, `Provide the "${key}" parameter.`);
64
+ }
65
+ }
66
+ for (const [key, value] of Object.entries(params)) {
67
+ if (!(key in schema_props)) {
68
+ throw new BuiltWithError('VALIDATION_ERROR', `Unknown parameter: "${key}".`, 0, null, `Remove the "${key}" parameter.`);
69
+ }
70
+ }
71
+ }
72
+
73
+ function _ok(data, raw, tool, request_id = null) {
74
+ return { ok: true, data, raw, error: null, meta: { request_id, tool, cached: null } };
75
+ }
76
+
77
+ function _err(error, tool = null) {
78
+ return { ok: false, data: null, raw: null, error: error.toJSON ? error.toJSON() : error, meta: { request_id: null, tool, cached: null } };
79
+ }
80
+
81
+ function _sleep(ms) {
82
+ return new Promise(resolve => setTimeout(resolve, ms));
83
+ }
84
+
85
+ function _parse_sse_body(raw_body) {
86
+ // If the body looks like SSE (starts with "event:" or "data:"), extract JSON from data lines
87
+ const trimmed = raw_body.trim();
88
+ if (trimmed.startsWith('event:') || trimmed.startsWith('data:')) {
89
+ const data_lines = trimmed.split('\n')
90
+ .filter(line => line.startsWith('data:'))
91
+ .map(line => line.substring(5).trim());
92
+ return data_lines.join('');
93
+ }
94
+ // Otherwise return as-is (plain JSON)
95
+ return raw_body;
96
+ }
97
+
98
+ // ── HTTP transport ─────────────────────────────────────────────────────────────
99
+
100
+ function _http_post(url_str, body, headers, timeout_ms) {
101
+ return new Promise((resolve, reject) => {
102
+ const parsed = new URL(url_str);
103
+ const payload = JSON.stringify(body);
104
+ const opts = {
105
+ hostname: parsed.hostname,
106
+ port: parsed.port || 443,
107
+ path: parsed.pathname + parsed.search,
108
+ method: 'POST',
109
+ headers: {
110
+ ...headers,
111
+ 'Accept': 'application/json, text/event-stream',
112
+ 'Content-Type': 'application/json',
113
+ 'Content-Length': Buffer.byteLength(payload),
114
+ },
115
+ timeout: timeout_ms,
116
+ };
117
+
118
+ const req = https.request(opts, (res) => {
119
+ const chunks = [];
120
+ res.on('data', chunk => chunks.push(chunk));
121
+ res.on('end', () => {
122
+ const raw_body = Buffer.concat(chunks).toString('utf-8');
123
+ resolve({ status: res.statusCode, headers: res.headers, body: raw_body });
124
+ });
125
+ });
126
+
127
+ req.on('error', reject);
128
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
129
+ req.write(payload);
130
+ req.end();
131
+ });
132
+ }
133
+
134
+ // ── Client ─────────────────────────────────────────────────────────────────────
135
+
136
+ class BuiltWithClient {
137
+ constructor(api_key, options = {}) {
138
+ if (!api_key || typeof api_key !== 'string') {
139
+ throw new Error('api_key is required');
140
+ }
141
+ this._api_key = api_key;
142
+ this._endpoint = options.endpoint || MCP_ENDPOINT;
143
+ this._max_retries = options.max_retries != null ? options.max_retries : MAX_RETRIES;
144
+ this._timeout_ms = options.timeout_ms || 30000;
145
+ }
146
+
147
+ // ── Request pipeline ───────────────────────────────────────────────────────
148
+
149
+ async _request(mcp_tool, params) {
150
+ const body = {
151
+ jsonrpc: '2.0',
152
+ method: 'tools/call',
153
+ params: { name: mcp_tool, arguments: params },
154
+ id: Date.now().toString(),
155
+ };
156
+
157
+ const headers = { Authorization: `Bearer ${this._api_key}` };
158
+
159
+ let last_error = null;
160
+ for (let attempt = 0; attempt <= this._max_retries; attempt++) {
161
+ try {
162
+ const res = await _http_post(this._endpoint, body, headers, this._timeout_ms);
163
+ const status = res.status;
164
+
165
+ // Retry on 429 or 5xx
166
+ if (status === 429 || status >= 500) {
167
+ const retry_after = res.headers['retry-after'];
168
+ const backoff = retry_after
169
+ ? parseInt(retry_after, 10) * 1000
170
+ : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
171
+
172
+ last_error = new BuiltWithError(
173
+ status === 429 ? 'RATE_LIMITED' : 'SERVER_ERROR',
174
+ `HTTP ${status}: ${res.body.substring(0, 200)}`,
175
+ status,
176
+ null,
177
+ status === 429 ? 'Reduce request rate or wait before retrying.' : 'The server encountered an error. Try again later.'
178
+ );
179
+
180
+ if (attempt < this._max_retries) {
181
+ await _sleep(backoff);
182
+ continue;
183
+ }
184
+ return _err(last_error, mcp_tool);
185
+ }
186
+
187
+ // Non-retryable errors
188
+ if (status === 401 || status === 403) {
189
+ return _err(new BuiltWithError('AUTH_ERROR', 'Authentication failed. Check your API key.', status, null, 'Verify your BuiltWith API key is correct and active.'), mcp_tool);
190
+ }
191
+
192
+ if (status < 200 || status >= 300) {
193
+ return _err(new BuiltWithError('HTTP_ERROR', `HTTP ${status}: ${res.body.substring(0, 200)}`, status), mcp_tool);
194
+ }
195
+
196
+ // Parse response (handle SSE format)
197
+ const json_body = _parse_sse_body(res.body);
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(json_body);
201
+ } catch (_) {
202
+ return _err(new BuiltWithError('PARSE_ERROR', 'Failed to parse response JSON.', status), mcp_tool);
203
+ }
204
+
205
+ // Handle JSON-RPC error
206
+ if (parsed.error) {
207
+ return _err(new BuiltWithError('MCP_ERROR', parsed.error.message || 'MCP error', status, parsed.error), mcp_tool);
208
+ }
209
+
210
+ // Extract result
211
+ const result = parsed.result;
212
+ let data = result;
213
+
214
+ // If result has content array (MCP standard), extract text
215
+ if (result && Array.isArray(result.content)) {
216
+ const text_parts = result.content.filter(c => c.type === 'text').map(c => c.text);
217
+ try {
218
+ data = JSON.parse(text_parts.join(''));
219
+ } catch (_) {
220
+ data = text_parts.join('');
221
+ }
222
+ }
223
+
224
+ return _ok(data, parsed, mcp_tool, body.id);
225
+
226
+ } catch (err) {
227
+ last_error = new BuiltWithError('NETWORK_ERROR', err.message, 0, null, 'Check network connectivity.');
228
+ if (attempt < this._max_retries) {
229
+ await _sleep(INITIAL_BACKOFF_MS * Math.pow(2, attempt));
230
+ continue;
231
+ }
232
+ return _err(last_error, mcp_tool);
233
+ }
234
+ }
235
+
236
+ return _err(last_error || new BuiltWithError('UNKNOWN_ERROR', 'Request failed', 0), mcp_tool);
237
+ }
238
+
239
+ // ── Public SDK methods ─────────────────────────────────────────────────────
240
+
241
+ async domain_lookup_live(params) {
242
+ const { domain, live_only = true } = params || {};
243
+ _validate_domain(domain);
244
+ return this._request('domain-lookup', { domain, liveOnly: live_only });
245
+ }
246
+
247
+ async domain_lookup(params) {
248
+ const { lookup } = params || {};
249
+ _validate_domain(lookup);
250
+ return this._request('domain-api', { lookup });
251
+ }
252
+
253
+ async relationships(params) {
254
+ const { lookup } = params || {};
255
+ _validate_domain(lookup);
256
+ return this._request('relationships-api', { lookup });
257
+ }
258
+
259
+ async free_summary(params) {
260
+ const { lookup } = params || {};
261
+ _validate_domain(lookup);
262
+ return this._request('free-api', { lookup });
263
+ }
264
+
265
+ async company_to_url(params) {
266
+ const { company } = params || {};
267
+ _validate_string('company', company);
268
+ return this._request('company-to-url', { company });
269
+ }
270
+
271
+ async tags_lookup(params) {
272
+ const { lookup } = params || {};
273
+ _validate_string('lookup', lookup);
274
+ return this._request('tags-api', { lookup });
275
+ }
276
+
277
+ async recommendations(params) {
278
+ const { lookup } = params || {};
279
+ _validate_domain(lookup);
280
+ return this._request('recommendations-api', { lookup });
281
+ }
282
+
283
+ async redirects(params) {
284
+ const { lookup } = params || {};
285
+ _validate_domain(lookup);
286
+ return this._request('redirects-api', { lookup });
287
+ }
288
+
289
+ async keywords(params) {
290
+ const { lookup } = params || {};
291
+ _validate_domain(lookup);
292
+ return this._request('keywords-api', { lookup });
293
+ }
294
+
295
+ async trends(params) {
296
+ const { tech } = params || {};
297
+ _validate_string('tech', tech);
298
+ return this._request('trends-api', { tech });
299
+ }
300
+
301
+ async product_search(params) {
302
+ const { query } = params || {};
303
+ _validate_string('query', query);
304
+ return this._request('product-api', { query });
305
+ }
306
+
307
+ async trust(params) {
308
+ const { lookup } = params || {};
309
+ _validate_domain(lookup);
310
+ return this._request('trust-api', { lookup });
311
+ }
312
+
313
+ async financial(params) {
314
+ const { lookup } = params || {};
315
+ _validate_domain(lookup);
316
+ return this._request('financial-api', { lookup });
317
+ }
318
+
319
+ async social(params) {
320
+ const { lookup } = params || {};
321
+ _validate_domain(lookup);
322
+ return this._request('social-api', { lookup });
323
+ }
324
+
325
+ // ── Prompt helpers ─────────────────────────────────────────────────────────
326
+
327
+ prompt_analyze_tech_stack(params) {
328
+ const { domain } = params || {};
329
+ _validate_domain(domain);
330
+ return { mcp_prompt: 'analyze-tech-stack', arguments: { domain } };
331
+ }
332
+
333
+ prompt_find_related_websites(params) {
334
+ const { domain } = params || {};
335
+ _validate_domain(domain);
336
+ return { mcp_prompt: 'find-related-websites', arguments: { domain } };
337
+ }
338
+
339
+ prompt_get_technology_recommendations(params) {
340
+ const { domain } = params || {};
341
+ _validate_domain(domain);
342
+ return { mcp_prompt: 'get-technology-recommendations', arguments: { domain } };
343
+ }
344
+
345
+ prompt_research_company(params) {
346
+ const { company } = params || {};
347
+ _validate_string('company', company);
348
+ return { mcp_prompt: 'research-company', arguments: { company } };
349
+ }
350
+
351
+ prompt_check_domain_trust(params) {
352
+ const { domain } = params || {};
353
+ _validate_domain(domain);
354
+ return { mcp_prompt: 'check-domain-trust', arguments: { domain } };
355
+ }
356
+ }
357
+
358
+ module.exports = { BuiltWithClient, BuiltWithError };