2manytabs-mcp-host 2.0.0 → 3.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.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/bridge.js +288 -120
- package/host.js +7 -21
- package/lib/tabs.js +29 -21
- package/package.json +34 -4
- package/tools/activate-tab.tool.js +30 -0
- package/tools/close-tabs.tool.js +40 -13
- package/tools/execute-script.tool.js +54 -0
- package/tools/get-tab-text.tool.js +31 -0
- package/tools/group-tabs.tool.js +36 -0
- package/tools/index.js +16 -4
- package/tools/list-tab-groups.tool.js +45 -0
- package/tools/list-tabs.tool.js +34 -74
- package/tools/open-tabs.tool.js +6 -4
- package/tools/present.js +131 -0
- package/tools/rendering.js +40 -0
- package/tools/ungroup-tabs.tool.js +28 -0
- package/tools/update-tab-group.tool.js +36 -0
- package/tools/update-tab.tool.js +36 -0
package/tools/present.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compact,
|
|
3
|
+
tabLabel,
|
|
4
|
+
TITLE_TRUNCATE_FULL,
|
|
5
|
+
HISTOGRAM_BAR_MAX,
|
|
6
|
+
} from '../lib/tabs.js';
|
|
7
|
+
import {
|
|
8
|
+
buildFlags,
|
|
9
|
+
formatFlags,
|
|
10
|
+
drawBar,
|
|
11
|
+
formatHistogramLine,
|
|
12
|
+
formatDomainHeader,
|
|
13
|
+
formatWindowHeader,
|
|
14
|
+
formatGroupPrefix,
|
|
15
|
+
formatIdPrefix,
|
|
16
|
+
} from './rendering.js';
|
|
17
|
+
|
|
18
|
+
function renderHistogram(histogram) {
|
|
19
|
+
const maxCount = Math.max(...histogram.map((x) => x.count));
|
|
20
|
+
const lines = ['Domains:'];
|
|
21
|
+
|
|
22
|
+
for (const { domain, count } of histogram) {
|
|
23
|
+
const bar = drawBar(count, maxCount, HISTOGRAM_BAR_MAX);
|
|
24
|
+
lines.push(formatHistogramLine(domain, count, bar));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderByDomain(tabs, groupMap) {
|
|
31
|
+
const grouped = {};
|
|
32
|
+
for (const t of tabs) {
|
|
33
|
+
const domain = t.url ? new URL(t.url).hostname : '(local)';
|
|
34
|
+
(grouped[domain] ??= []).push(t);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = [];
|
|
38
|
+
for (const [domain, tabsInDomain] of Object.entries(grouped)) {
|
|
39
|
+
lines.push(formatDomainHeader(domain, tabsInDomain.length));
|
|
40
|
+
for (const t of tabsInDomain) {
|
|
41
|
+
const label = tabLabel(t);
|
|
42
|
+
const flags = buildFlags(t);
|
|
43
|
+
const flagStr = formatFlags(flags);
|
|
44
|
+
lines.push(` · ${formatIdPrefix(t)}${formatGroupPrefix(groupMap, t)}${label}${flagStr}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderByWindow(tabs, groupMap) {
|
|
53
|
+
const grouped = {};
|
|
54
|
+
for (const t of tabs) {
|
|
55
|
+
const key = t.browser ? `${t.browser}:${t.windowId}` : `${t.windowId}`;
|
|
56
|
+
(grouped[key] ??= []).push(t);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines = [];
|
|
60
|
+
for (const [key, tabsInWin] of Object.entries(grouped)) {
|
|
61
|
+
const browser = tabsInWin[0]?.browser;
|
|
62
|
+
const winId = tabsInWin[0]?.windowId ?? key;
|
|
63
|
+
lines.push(formatWindowHeader(winId, tabsInWin.length, browser));
|
|
64
|
+
for (const t of tabsInWin) {
|
|
65
|
+
const label = tabLabel(t);
|
|
66
|
+
const flags = buildFlags(t);
|
|
67
|
+
const flagStr = formatFlags(flags);
|
|
68
|
+
lines.push(` · ${formatIdPrefix(t)}${formatGroupPrefix(groupMap, t)}${label}${flagStr}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderFlat(tabs, groupMap) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
for (const t of tabs) {
|
|
79
|
+
const c = compact(t);
|
|
80
|
+
const label =
|
|
81
|
+
c.title.length > TITLE_TRUNCATE_FULL
|
|
82
|
+
? c.title.slice(0, TITLE_TRUNCATE_FULL - 3) + '…'
|
|
83
|
+
: c.title;
|
|
84
|
+
const flags = buildFlags(c);
|
|
85
|
+
const flagStr = formatFlags(flags);
|
|
86
|
+
lines.push(` · ${formatIdPrefix(t)}${formatGroupPrefix(groupMap, t)}${label}${flagStr}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatTabData(data) {
|
|
93
|
+
const {
|
|
94
|
+
tabs,
|
|
95
|
+
histogram,
|
|
96
|
+
groupMap,
|
|
97
|
+
group_by,
|
|
98
|
+
query,
|
|
99
|
+
duplicates_only,
|
|
100
|
+
windows,
|
|
101
|
+
dupCount,
|
|
102
|
+
} = data;
|
|
103
|
+
|
|
104
|
+
const lines = [];
|
|
105
|
+
|
|
106
|
+
const browsers = [...new Set(tabs.map((t) => t.browser).filter(Boolean))];
|
|
107
|
+
const browserSummary = browsers.length > 1
|
|
108
|
+
? ` across ${browsers.length} browsers (${browsers.map((b) => `${b}: ${tabs.filter((t) => t.browser === b).length}`).join(', ')}) and`
|
|
109
|
+
: ' across';
|
|
110
|
+
|
|
111
|
+
lines.push(
|
|
112
|
+
`📊 ${tabs.length} tab(s)${browserSummary} ${windows} window(s)` +
|
|
113
|
+
(query ? ` matching "${query}"` : '') +
|
|
114
|
+
(duplicates_only ? ' (duplicates only)' : '') +
|
|
115
|
+
(dupCount > 0 ? ` · ${dupCount} duplicate(s) found` : '') +
|
|
116
|
+
'\n',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
lines.push(...renderHistogram(histogram));
|
|
120
|
+
lines.push('');
|
|
121
|
+
|
|
122
|
+
if (group_by === 'domain') {
|
|
123
|
+
lines.push(...renderByDomain(tabs, groupMap));
|
|
124
|
+
} else if (group_by === 'window') {
|
|
125
|
+
lines.push(...renderByWindow(tabs, groupMap));
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(...renderFlat(tabs, groupMap));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function buildFlags(tab) {
|
|
2
|
+
const flags = [];
|
|
3
|
+
if (tab.pinned) flags.push('📌');
|
|
4
|
+
if (tab.audible) flags.push('🔊');
|
|
5
|
+
return flags;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatFlags(flags) {
|
|
9
|
+
return flags.length ? ' ' + flags.join('') : '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function drawBar(count, maxCount, maxWidth) {
|
|
13
|
+
return '▇'.repeat(Math.max(1, Math.round((count / maxCount) * maxWidth)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatHistogramLine(domain, count, bar) {
|
|
17
|
+
return ` ${bar} ${domain.padEnd(28)} ${count} tab(s)`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatDomainHeader(domain, count) {
|
|
21
|
+
return `📁 ${domain} — ${count} tab(s)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatWindowHeader(winId, count, browser) {
|
|
25
|
+
const browserPrefix = browser ? `[${browser.charAt(0).toUpperCase() + browser.slice(1)}] ` : '';
|
|
26
|
+
return `🪟 ${browserPrefix}Window ${winId} — ${count} tab(s)`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatGroupPrefix(groupMap, tab) {
|
|
30
|
+
if (tab.groupId === undefined || tab.groupId === -1) return '';
|
|
31
|
+
const g = groupMap.get(tab.browser ? `${tab.browser}:${tab.groupId}` : tab.groupId);
|
|
32
|
+
return g ? `[Group: ${g.title || 'Group ' + g.id}] ` : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatIdPrefix(tab) {
|
|
36
|
+
if (tab.browser) {
|
|
37
|
+
return `[id:${tab.browser}:${tab.id}] `;
|
|
38
|
+
}
|
|
39
|
+
return `[id:${tab.id}] `;
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { callExtension } from '../bridge.js';
|
|
3
|
+
|
|
4
|
+
export const ungroupTabsTool = {
|
|
5
|
+
name: 'ungroup_tabs',
|
|
6
|
+
title: 'Ungroup Tabs',
|
|
7
|
+
description: 'Remove one or more open tabs from their current native tab groups.',
|
|
8
|
+
annotations: {
|
|
9
|
+
readOnlyHint: false,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: false,
|
|
12
|
+
openWorldHint: true,
|
|
13
|
+
},
|
|
14
|
+
inputSchema: {
|
|
15
|
+
tab_ids: z.array(z.union([z.number().int(), z.string()])).min(1)
|
|
16
|
+
.describe('Tab IDs to remove from any groups (numbers or composite like "chrome:12").'),
|
|
17
|
+
browser: z.string().optional()
|
|
18
|
+
.describe('Optional browser hint ("chrome", "firefox") if ids are numeric.'),
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
execute: async ({ tab_ids, browser }) => {
|
|
22
|
+
const res = await callExtension('ungroup_tabs', { tab_ids, browser });
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
message: `Successfully ungrouped ${res.ungrouped} tab(s).`,
|
|
25
|
+
ungrouped: res.ungrouped,
|
|
26
|
+
}, null, 2);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { callExtension } from '../bridge.js';
|
|
3
|
+
|
|
4
|
+
export const updateTabGroupTool = {
|
|
5
|
+
name: 'update_tab_group',
|
|
6
|
+
title: 'Update Tab Group',
|
|
7
|
+
description:
|
|
8
|
+
'Update properties of an existing native browser tab group, including ' +
|
|
9
|
+
'its title, color, or collapsed/expanded state.',
|
|
10
|
+
annotations: {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: false,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
},
|
|
16
|
+
inputSchema: {
|
|
17
|
+
group_id: z.union([z.number().int(), z.string()])
|
|
18
|
+
.describe('The ID (number or composite like "chrome:5") of the tab group to update (obtained from list_tab_groups or list_tabs).'),
|
|
19
|
+
title: z.string().optional()
|
|
20
|
+
.describe('New title for the group.'),
|
|
21
|
+
color: z.enum(['grey', 'blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan', 'orange']).optional()
|
|
22
|
+
.describe('New color for the group.'),
|
|
23
|
+
collapsed: z.boolean().optional()
|
|
24
|
+
.describe('Whether the group should be collapsed (true) or expanded (false).'),
|
|
25
|
+
browser: z.string().optional()
|
|
26
|
+
.describe('Optional browser hint ("chrome", "firefox") if ids are numeric.'),
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
execute: async ({ group_id, title, color, collapsed, browser }) => {
|
|
30
|
+
const res = await callExtension('update_group', { group_id, title, color, collapsed, browser });
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
message: `Successfully updated tab group ${group_id}.`,
|
|
33
|
+
groupId: res.updated,
|
|
34
|
+
}, null, 2);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { callExtension } from '../bridge.js';
|
|
3
|
+
|
|
4
|
+
export const updateTabTool = {
|
|
5
|
+
name: 'update_tab',
|
|
6
|
+
title: 'Update Tab',
|
|
7
|
+
description:
|
|
8
|
+
'Modify properties of an open browser tab, such as navigating it to a new URL, ' +
|
|
9
|
+
'pinning/unpinning it, or muting/unmuting its audio.',
|
|
10
|
+
annotations: {
|
|
11
|
+
readOnlyHint: false,
|
|
12
|
+
destructiveHint: false,
|
|
13
|
+
idempotentHint: false,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
},
|
|
16
|
+
inputSchema: {
|
|
17
|
+
tab_id: z.union([z.number().int(), z.string()])
|
|
18
|
+
.describe('The numeric ID (or composite like "chrome:123") of the tab to update.'),
|
|
19
|
+
url: z.string().optional()
|
|
20
|
+
.describe('A new URL to navigate the tab to. If no protocol is provided, https:// will be prepended.'),
|
|
21
|
+
pinned: z.boolean().optional()
|
|
22
|
+
.describe('Set to true to pin the tab, or false to unpin it.'),
|
|
23
|
+
muted: z.boolean().optional()
|
|
24
|
+
.describe('Set to true to mute the tab, or false to unmute it.'),
|
|
25
|
+
browser: z.string().optional()
|
|
26
|
+
.describe('Optional browser hint ("chrome", "firefox") if tab_id is numeric.'),
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
execute: async ({ tab_id, url, pinned, muted, browser }) => {
|
|
30
|
+
const res = await callExtension('update_tab', { tab_id, url, pinned, muted, browser });
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
message: `Successfully updated tab ${tab_id}.`,
|
|
33
|
+
tabId: res.updated,
|
|
34
|
+
}, null, 2);
|
|
35
|
+
},
|
|
36
|
+
};
|