@pablovitasso/szkrabok 1.0.10

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/package.json +124 -0
  4. package/packages/runtime/config.js +173 -0
  5. package/packages/runtime/index.js +10 -0
  6. package/packages/runtime/launch.js +240 -0
  7. package/packages/runtime/logger.js +42 -0
  8. package/packages/runtime/mcp-client/adapters/szkrabok-session.js +69 -0
  9. package/packages/runtime/mcp-client/codegen/generate-mcp-tools.mjs +66 -0
  10. package/packages/runtime/mcp-client/codegen/render-tools.js +219 -0
  11. package/packages/runtime/mcp-client/codegen/schema-to-jsdoc.js +60 -0
  12. package/packages/runtime/mcp-client/mcp-tools.d.ts +92 -0
  13. package/packages/runtime/mcp-client/mcp-tools.js +99 -0
  14. package/packages/runtime/mcp-client/runtime/invoker.js +95 -0
  15. package/packages/runtime/mcp-client/runtime/logger.js +145 -0
  16. package/packages/runtime/mcp-client/runtime/transport.js +35 -0
  17. package/packages/runtime/package.json +25 -0
  18. package/packages/runtime/pool.js +59 -0
  19. package/packages/runtime/scripts/patch-playwright.js +736 -0
  20. package/packages/runtime/sessions.js +77 -0
  21. package/packages/runtime/stealth.js +232 -0
  22. package/packages/runtime/storage.js +64 -0
  23. package/scripts/detect_browsers.sh +147 -0
  24. package/scripts/patch-playwright.js +736 -0
  25. package/scripts/postinstall.js +47 -0
  26. package/scripts/release-publish.js +19 -0
  27. package/scripts/release-reminder.js +14 -0
  28. package/scripts/setup.js +17 -0
  29. package/src/cli.js +166 -0
  30. package/src/config.js +36 -0
  31. package/src/index.js +53 -0
  32. package/src/server.js +40 -0
  33. package/src/tools/registry.js +171 -0
  34. package/src/tools/scaffold.js +133 -0
  35. package/src/tools/szkrabok_browser.js +227 -0
  36. package/src/tools/szkrabok_session.js +174 -0
  37. package/src/tools/templates/automation/example.mcp.spec.js +54 -0
  38. package/src/tools/templates/automation/example.spec.js +29 -0
  39. package/src/tools/templates/automation/fixtures.js +59 -0
  40. package/src/tools/templates/playwright.config.js +10 -0
  41. package/src/tools/templates/szkrabok.config.local.toml.example +12 -0
  42. package/src/tools/workflow.js +45 -0
  43. package/src/utils/errors.js +36 -0
  44. package/src/utils/logger.js +64 -0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Smart Chromium installer — runs after npm install.
3
+ // Skips if: CI env, SZKRABOK_SKIP_BROWSER_INSTALL set, or Chromium already found.
4
+
5
+ import { execSync } from 'node:child_process';
6
+ import { existsSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+
10
+ if (process.env.CI || process.env.SZKRABOK_SKIP_BROWSER_INSTALL) {
11
+ console.log('szkrabok: skipping browser install (CI or SZKRABOK_SKIP_BROWSER_INSTALL set).');
12
+ process.exit(0);
13
+ }
14
+
15
+ const findChromium = () => {
16
+ const playwrightCache = join(homedir(), '.cache', 'ms-playwright');
17
+ if (existsSync(playwrightCache)) {
18
+ const dirs = readdirSync(playwrightCache)
19
+ .filter(d => d.startsWith('chromium-'))
20
+ .sort()
21
+ .reverse();
22
+ for (const dir of dirs) {
23
+ for (const bin of ['chrome-linux/chrome', 'chrome-linux64/chrome']) {
24
+ const p = join(playwrightCache, dir, bin);
25
+ if (existsSync(p)) return p;
26
+ }
27
+ }
28
+ }
29
+ for (const p of ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome']) {
30
+ if (existsSync(p)) return p;
31
+ }
32
+ return null;
33
+ };
34
+
35
+ if (findChromium()) {
36
+ console.log('szkrabok: Chromium already available, skipping install.');
37
+ process.exit(0);
38
+ }
39
+
40
+ console.log('szkrabok: installing Playwright Chromium browser...');
41
+ try {
42
+ execSync('npx playwright install chromium', { stdio: 'inherit' });
43
+ } catch {
44
+ console.error(
45
+ 'szkrabok: browser install failed. Run "szkrabok --setup" manually to retry.'
46
+ );
47
+ }
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const pkg = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'));
7
+ const v = pkg.version;
8
+ const tarball = `dist/szkrabok-runtime-${v}.tgz`;
9
+
10
+ try {
11
+ execSync(`gh auth status`, { stdio: 'pipe' });
12
+ } catch {
13
+ console.error('ERROR: gh auth failed. Run: gh auth login');
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(`Creating GitHub release v${v} and uploading ${tarball}...`);
18
+ execSync(`gh release create v${v} ${tarball} --repo PabloVitasso/szkrabok --title v${v}`, { stdio: 'inherit' });
19
+ console.log(`\nDone. Remember to update RUNTIME_RELEASES in src/tools/scaffold.js.`);
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const pkg = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'));
6
+ const v = pkg.version;
7
+
8
+ console.log(`
9
+ Pack complete: dist/szkrabok-runtime-${v}.tgz
10
+
11
+ Next steps:
12
+ 1. npm run release:publish
13
+ 2. Update RUNTIME_RELEASES in src/tools/scaffold.js (add '${v}' entry, bump CURRENT_RUNTIME_VERSION)
14
+ `);
@@ -0,0 +1,17 @@
1
+ import { mkdir } from 'fs/promises'
2
+ import { existsSync } from 'fs'
3
+
4
+ const dirs = ['sessions', 'logs']
5
+
6
+ const setup = async () => {
7
+ for (const dir of dirs) {
8
+ if (!existsSync(dir)) {
9
+ await mkdir(dir, { recursive: true })
10
+ console.log(`Created ${dir}/`)
11
+ }
12
+ }
13
+
14
+ console.log('szkrabok installed. To install the Chromium browser, run: szkrabok --setup')
15
+ }
16
+
17
+ setup().catch(console.error)
package/src/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs/promises';
6
+ import { list, deleteSession, endpoint } from './tools/szkrabok_session.js';
7
+
8
+ const SESSIONS_DIR = path.join(process.cwd(), 'sessions');
9
+
10
+ program
11
+ .name('bebok')
12
+ .description('szkrabok CLI')
13
+ .version('2.0.0');
14
+
15
+ /* ---------- helpers ---------- */
16
+
17
+ const readJson = async file => {
18
+ try {
19
+ return JSON.parse(await fs.readFile(file, 'utf8'));
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+
25
+ const shutdownHandler = handle => async () => {
26
+ try {
27
+ await handle.close();
28
+ } finally {
29
+ process.exit(0);
30
+ }
31
+ };
32
+
33
+ /* ---------- session command ---------- */
34
+
35
+ program
36
+ .command('session')
37
+ .description('Session management')
38
+ .argument('<action>', 'list | inspect | delete | cleanup')
39
+ .argument('[id]', 'Session ID')
40
+ .option('--days <days>', 'For cleanup: delete sessions older than N days', '30')
41
+ .action(async (action, id, options) => {
42
+ const actions = {
43
+ list: async () => {
44
+ const { sessions } = await list();
45
+ console.table(
46
+ sessions.map(s => ({
47
+ ID: s.id,
48
+ Active: s.active ? 'yes' : 'no',
49
+ Preset: s.preset ?? 'N/A',
50
+ Label: s.label ?? 'N/A',
51
+ }))
52
+ );
53
+ },
54
+
55
+ inspect: async () => {
56
+ if (!id) throw new Error('Session ID required');
57
+
58
+ const dir = path.join(SESSIONS_DIR, id);
59
+ const [meta, state] = await Promise.all([
60
+ readJson(path.join(dir, 'meta.json')),
61
+ readJson(path.join(dir, 'state.json')),
62
+ ]);
63
+
64
+ if (!meta || !state) throw new Error(`Session ${id} not found or incomplete`);
65
+
66
+ console.log('=== METADATA ===');
67
+ console.log(JSON.stringify(meta, null, 2));
68
+ console.log('\n=== COOKIES ===');
69
+ console.log(state.cookies?.length ?? 0, 'cookies');
70
+ console.log('\n=== LOCALSTORAGE ===');
71
+ for (const origin of state.origins ?? []) {
72
+ console.log(origin.origin, ':', origin.localStorage?.length ?? 0, 'items');
73
+ }
74
+ },
75
+
76
+ delete: async () => {
77
+ if (!id) throw new Error('Session ID required');
78
+ await deleteSession({ sessionName: id });
79
+ console.log(`Session ${id} deleted`);
80
+ },
81
+
82
+ cleanup: async () => {
83
+ const days = Number(options.days) || 30;
84
+ const cutoff = Date.now() - days * 86400000;
85
+
86
+ let entries;
87
+ try {
88
+ entries = await fs.readdir(SESSIONS_DIR, { withFileTypes: true });
89
+ } catch {
90
+ console.log('No sessions directory found');
91
+ return;
92
+ }
93
+
94
+ await Promise.all(
95
+ entries
96
+ .filter(e => e.isDirectory())
97
+ .map(async e => {
98
+ const meta = await readJson(path.join(SESSIONS_DIR, e.name, 'meta.json'));
99
+ if (meta?.lastUsed && meta.lastUsed < cutoff) {
100
+ await deleteSession({ sessionName: e.name });
101
+ console.log(`Deleted old session: ${e.name}`);
102
+ }
103
+ })
104
+ );
105
+ },
106
+ };
107
+
108
+ const fn = actions[action];
109
+ if (!fn) {
110
+ console.error(`Unknown action: ${action}. Use: list | inspect | delete | cleanup`);
111
+ process.exit(1);
112
+ }
113
+
114
+ try {
115
+ await fn();
116
+ } catch (err) {
117
+ console.error(err.message);
118
+ process.exit(1);
119
+ }
120
+ });
121
+
122
+ /* ---------- open command ---------- */
123
+
124
+ program
125
+ .command('open <profile>')
126
+ .description(
127
+ 'Launch a browser session with stealth + persistence. Prints CDP endpoint. Runs until Ctrl-C.'
128
+ )
129
+ .option('--preset <preset>', 'TOML preset name')
130
+ .option('--headless', 'Run headless')
131
+ .action(async (profile, options) => {
132
+ const { launch } = await import('#runtime');
133
+
134
+ const handle = await launch({
135
+ profile,
136
+ preset: options.preset,
137
+ headless: options.headless ?? undefined,
138
+ reuse: false,
139
+ });
140
+
141
+ console.log(handle.cdpEndpoint);
142
+
143
+ const shutdown = shutdownHandler(handle);
144
+ process.on('SIGINT', shutdown);
145
+ process.on('SIGTERM', shutdown);
146
+
147
+ await new Promise(() => {});
148
+ });
149
+
150
+ /* ---------- endpoint command ---------- */
151
+
152
+ program
153
+ .command('endpoint <sessionName>')
154
+ .description('Print CDP and WS endpoints for a running session')
155
+ .action(async sessionName => {
156
+ try {
157
+ const result = await endpoint({ sessionName });
158
+ console.log(`CDP: ${result.cdpEndpoint}`);
159
+ if (result.wsEndpoint) console.log(`WS: ${result.wsEndpoint}`);
160
+ } catch (err) {
161
+ console.error(err.message);
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ program.parse();
package/src/config.js ADDED
@@ -0,0 +1,36 @@
1
+ // MCP-layer config — request timeouts and logging only.
2
+ // Browser launch config (presets, stealth, headless, UA, viewport) lives in
3
+ // @szkrabok/runtime. Do not re-add browser concerns here.
4
+
5
+ import { join, resolve, dirname } from 'path';
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import { parse } from 'smol-toml';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const REPO_ROOT = resolve(__dirname, '..');
12
+
13
+ const isPlainObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
14
+ const deepMerge = (base, override) => {
15
+ const result = { ...base };
16
+ for (const key of Object.keys(override)) {
17
+ result[key] =
18
+ isPlainObject(base[key]) && isPlainObject(override[key])
19
+ ? deepMerge(base[key], override[key])
20
+ : override[key];
21
+ }
22
+ return result;
23
+ };
24
+
25
+ const TOML_PATH = join(REPO_ROOT, 'szkrabok.config.toml');
26
+ const TOML_LOCAL_PATH = join(REPO_ROOT, 'szkrabok.config.local.toml');
27
+ const tomlBase = existsSync(TOML_PATH) ? parse(readFileSync(TOML_PATH, 'utf8')) : {};
28
+ const tomlLocal = existsSync(TOML_LOCAL_PATH) ? parse(readFileSync(TOML_LOCAL_PATH, 'utf8')) : {};
29
+ const toml = deepMerge(tomlBase, tomlLocal);
30
+
31
+ const tomlDefault = toml.default ?? {};
32
+
33
+ export const DEFAULT_TIMEOUT = 30000;
34
+ export const TIMEOUT = tomlDefault.timeout ?? DEFAULT_TIMEOUT;
35
+ export const LOG_LEVEL = tomlDefault.log_level ?? 'info';
36
+ export const DISABLE_WEBGL = tomlDefault.disable_webgl ?? false;
package/src/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { createServer } from './server.js';
4
+ import { log, logError } from './utils/logger.js';
5
+
6
+ // Parse CLI args
7
+ const args = process.argv.slice(2);
8
+
9
+ if (args.includes('init')) {
10
+ const { init } = await import('./tools/scaffold.js');
11
+ const result = await init({ dir: process.cwd(), preset: 'minimal', install: false });
12
+ if (result.created.length) console.error(`Created: ${result.created.join(', ')}`);
13
+ if (result.merged.length) console.error(`Merged: ${result.merged.join(', ')}`);
14
+ if (result.skipped.length) console.error(`Skipped (already exists): ${result.skipped.join(', ')}`);
15
+ if (result.warnings.length) result.warnings.forEach(w => console.error(`Warning: ${w}`));
16
+ console.error('Done. Run "szkrabok --setup" if Chromium is not yet installed.');
17
+ process.exit(0);
18
+ }
19
+
20
+ if (args.includes('--setup')) {
21
+ const { execSync } = await import('node:child_process');
22
+ console.log('Installing Playwright Chromium browser...');
23
+ try {
24
+ execSync('npx playwright install chromium', { stdio: 'inherit' });
25
+ console.log('Browser installed successfully.');
26
+ } catch {
27
+ console.error('Browser install failed. Run manually: npx playwright install chromium');
28
+ process.exit(1);
29
+ }
30
+ process.exit(0);
31
+ }
32
+
33
+ if (args.includes('--no-headless') || args.includes('--headful')) {
34
+ process.env.HEADLESS = 'false';
35
+ }
36
+
37
+ const server = createServer();
38
+
39
+ process.on('SIGINT', async () => {
40
+ log('Shutting down gracefully...');
41
+ await server.close();
42
+ process.exit(0);
43
+ });
44
+
45
+ process.on('uncaughtException', err => {
46
+ logError('Uncaught exception', err);
47
+ process.exit(1);
48
+ });
49
+
50
+ server.connect().catch(err => {
51
+ logError('Failed to start server', err);
52
+ process.exit(1);
53
+ });
package/src/server.js ADDED
@@ -0,0 +1,40 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { registerTools, handleToolCall } from './tools/registry.js';
5
+ import { closeAllSessions } from '#runtime';
6
+ import { log } from './utils/logger.js';
7
+
8
+ export const createServer = () => {
9
+ const server = new Server(
10
+ {
11
+ name: 'szkrabok',
12
+ version: '2.0.0',
13
+ },
14
+ {
15
+ capabilities: {
16
+ tools: {},
17
+ },
18
+ }
19
+ );
20
+
21
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
22
+ tools: registerTools(),
23
+ }));
24
+
25
+ server.setRequestHandler(CallToolRequestSchema, async request =>
26
+ handleToolCall(request.params.name, request.params.arguments)
27
+ );
28
+
29
+ return {
30
+ async connect() {
31
+ const transport = new StdioServerTransport();
32
+ await server.connect(transport);
33
+ log('Server connected via stdio');
34
+ },
35
+ async close() {
36
+ await closeAllSessions();
37
+ log('Server closed');
38
+ },
39
+ };
40
+ };
@@ -0,0 +1,171 @@
1
+ import * as session from './szkrabok_session.js';
2
+ import * as workflow from './workflow.js';
3
+ import * as szkrabokBrowser from './szkrabok_browser.js';
4
+ import * as scaffold from './scaffold.js';
5
+ import { wrapError } from '../utils/errors.js';
6
+ import { logError } from '../utils/logger.js';
7
+
8
+ const SZKRABOK = '[szkrabok]';
9
+ const PLAYWRIGHT_MCP = '[playwright-mcp]';
10
+
11
+ const tools = {
12
+ 'session_manage': {
13
+ handler: session.manage,
14
+ description: `${SZKRABOK} Manage browser sessions. action: open (launch/resume session), close (save+close), list (all stored), delete (remove data), endpoint (get CDP/WS URLs). open requires sessionName; list requires none; others require sessionName`,
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ action: {
19
+ type: 'string',
20
+ enum: ['open', 'close', 'list', 'delete', 'endpoint'],
21
+ },
22
+ sessionName: { type: 'string' },
23
+ url: { type: 'string' },
24
+ save: { type: 'boolean', default: true },
25
+ launchOptions: {
26
+ type: 'object',
27
+ description:
28
+ 'open only. Use either preset OR individual fields (userAgent, viewport, locale, timezone). headless and stealth always allowed.',
29
+ properties: {
30
+ preset: { type: 'string', description: 'Preset name from szkrabok.config.toml' },
31
+ stealth: { type: 'boolean', default: true },
32
+ disableWebGL: { type: 'boolean', default: false },
33
+ headless: { type: 'boolean' },
34
+ userAgent: { type: 'string' },
35
+ viewport: {
36
+ type: 'object',
37
+ properties: { width: { type: 'number' }, height: { type: 'number' } },
38
+ },
39
+ locale: { type: 'string' },
40
+ timezone: { type: 'string' },
41
+ },
42
+ },
43
+ },
44
+ required: ['action'],
45
+ },
46
+ },
47
+
48
+ 'workflow.scrape': {
49
+ handler: workflow.scrape,
50
+ description: `${SZKRABOK} Scrape current page into LLM-ready text. Returns raw blocks and llmFriendly string. selectors: optional CSS selectors to target specific areas; omit for auto (main/body)`,
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ sessionName: { type: 'string' },
55
+ selectors: {
56
+ type: 'array',
57
+ items: { type: 'string' },
58
+ description: 'CSS selectors to target. Omit for auto-mode (main or body).',
59
+ },
60
+ },
61
+ required: ['sessionName'],
62
+ },
63
+ },
64
+
65
+ 'scaffold.init': {
66
+ handler: scaffold.init,
67
+ description: `${SZKRABOK} Init szkrabok project (idempotent). Prerequisite for browser runs. minimal (default): config/deps; full: automation fixtures and Playwright specs`,
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ dir: { type: 'string', description: 'Target directory. Defaults to cwd.' },
72
+ name: { type: 'string', description: 'Package name. Defaults to dirname.' },
73
+ preset: {
74
+ type: 'string',
75
+ enum: ['minimal', 'full'],
76
+ description: 'minimal (default): config files only. full: + automation/fixtures.js + automation/example.spec.js + automation/example.mcp.spec.js',
77
+ },
78
+ install: {
79
+ type: 'boolean',
80
+ description: 'Run npm install after writing files. Default false.',
81
+ },
82
+ },
83
+ },
84
+ },
85
+
86
+ 'browser_run': {
87
+ handler: szkrabokBrowser.run,
88
+ description: `${PLAYWRIGHT_MCP} Execute Playwright JS on session page. Pass code (inline snippet) or path (named export from .mjs file with (page, args)). fn defaults to "default".`,
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ sessionName: { type: 'string' },
93
+ code: { type: 'string' },
94
+ path: { type: 'string', description: 'Absolute or relative path to an .mjs script file' },
95
+ fn: { type: 'string', description: 'Named export to call. Defaults to "default".' },
96
+ args: { type: 'object', description: 'Arguments passed as second parameter to the function' },
97
+ },
98
+ required: ['sessionName'],
99
+ },
100
+ },
101
+
102
+ 'browser.run_test': {
103
+ handler: szkrabokBrowser.run_test,
104
+ description: `${PLAYWRIGHT_MCP} Run .spec.js tests via CDP (returns JSON). Requires session_manage(open) and scaffold.init`,
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ sessionName: { type: 'string' },
109
+ grep: { type: 'string', description: 'Filter tests by name (regex)' },
110
+ params: {
111
+ type: 'object',
112
+ description:
113
+ 'Key/value params passed as TEST_* env vars to the spec (e.g. {url:"https://..."} → TEST_URL)',
114
+ },
115
+ config: {
116
+ type: 'string',
117
+ description: 'Config path relative to repo root. Defaults to playwright.config.js',
118
+ },
119
+ project: {
120
+ type: 'string',
121
+ description:
122
+ 'Playwright project name to run (e.g. "automation"). Runs all projects if omitted.',
123
+ },
124
+ files: {
125
+ type: 'array',
126
+ items: { type: 'string' },
127
+ description:
128
+ 'File or directory paths passed as positional args to playwright test (e.g. ["automation/rebrowser-check.spec.js"] or ["automation/"]). Relative to repo root.',
129
+ },
130
+ keepOpen: {
131
+ type: 'boolean',
132
+ description:
133
+ 'After the test run, reconnect the session if the test subprocess invalidated the MCP context. Chrome stays alive; this restores the Playwright connection to it. Default false.',
134
+ },
135
+ },
136
+ required: ['sessionName'],
137
+ },
138
+ },
139
+
140
+ };
141
+
142
+ export const registerTools = () =>
143
+ Object.entries(tools).map(([name, tool]) => ({
144
+ name,
145
+ description: tool.description,
146
+ inputSchema: tool.inputSchema,
147
+ }));
148
+
149
+ export const handleToolCall = async (name, args) => {
150
+ const tool = tools[name];
151
+
152
+ if (!tool) {
153
+ return {
154
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
155
+ isError: true,
156
+ };
157
+ }
158
+
159
+ try {
160
+ const result = await tool.handler(args);
161
+ return {
162
+ content: [{ type: 'text', text: JSON.stringify(result) }],
163
+ };
164
+ } catch (err) {
165
+ logError(`Tool ${name} failed`, err, { args });
166
+ return {
167
+ content: [{ type: 'text', text: JSON.stringify(wrapError(err)) }],
168
+ isError: true,
169
+ };
170
+ }
171
+ };