@agenticmail/enterprise 0.5.241 → 0.5.243

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.
@@ -3,515 +3,734 @@ import { I } from '../components/icons.js';
3
3
  import { Modal } from '../components/modal.js';
4
4
  import { HelpButton } from '../components/help-button.js';
5
5
 
6
- // Auth help: where to get API keys/tokens for each skill
7
- var AUTH_HELP = {
8
- 'intercom-support': { provider: 'Intercom', url: 'https://developers.intercom.com/docs/build-an-integration/getting-started/', steps: 'Create an Intercom app at developers.intercom.com, then copy the Access Token from Authentication.' },
9
- 'zendesk-tickets': { provider: 'Zendesk', url: 'https://support.zendesk.com/hc/en-us/articles/4408889192858', steps: 'Go to Zendesk Admin > Apps & Integrations > APIs > Zendesk API, then generate an API token.' },
10
- 'github-issues': { provider: 'GitHub', url: 'https://github.com/settings/tokens', steps: 'Go to GitHub Settings > Developer Settings > Personal Access Tokens > Generate new token. Select repo and issues scopes.' },
11
- 'github-actions': { provider: 'GitHub', url: 'https://github.com/settings/tokens', steps: 'Generate a Personal Access Token with workflow and actions:read scopes.' },
12
- 'slack-notifications': { provider: 'Slack', url: 'https://api.slack.com/apps', steps: 'Create a Slack app at api.slack.com/apps, install to workspace, copy the Bot User OAuth Token (xoxb-...).' },
13
- 'jira-integration': { provider: 'Atlassian', url: 'https://id.atlassian.com/manage-profile/security/api-tokens', steps: 'Go to Atlassian account > Security > API tokens > Create API token. Use with your email as username.' },
14
- 'stripe-billing': { provider: 'Stripe', url: 'https://dashboard.stripe.com/apikeys', steps: 'Go to Stripe Dashboard > Developers > API keys. Copy the Secret key (sk_live_... or sk_test_...).' },
15
- 'notion-sync': { provider: 'Notion', url: 'https://www.notion.so/my-integrations', steps: 'Create an integration at notion.so/my-integrations. Copy the Internal Integration Secret. Share pages with the integration.' },
16
- 'salesforce-crm': { provider: 'Salesforce', url: 'https://help.salesforce.com/s/articleView?id=sf.connected_app_create_api_integration.htm', steps: 'Create a Connected App in Salesforce Setup. Use OAuth 2.0 client credentials flow. Copy Consumer Key and Secret.' },
17
- 'hubspot-crm': { provider: 'HubSpot', url: 'https://developers.hubspot.com/docs/api/private-apps', steps: 'Go to HubSpot Settings > Integrations > Private Apps > Create. Select required scopes and copy the access token.' },
18
- 'twilio-sms': { provider: 'Twilio', url: 'https://console.twilio.com/', steps: 'Sign up at twilio.com. Copy Account SID and Auth Token from the Console dashboard. Get a phone number for SMS.' },
19
- 'sendgrid-email': { provider: 'SendGrid', url: 'https://app.sendgrid.com/settings/api_keys', steps: 'Go to SendGrid Settings > API Keys > Create API Key. Select Full Access or restricted permissions.' },
20
- 'google-workspace': { provider: 'Google', url: 'https://console.cloud.google.com/apis/credentials', steps: 'Create a project in Google Cloud Console. Enable required APIs. Create OAuth 2.0 credentials or a Service Account key.' },
21
- 'shopify-store': { provider: 'Shopify', url: 'https://partners.shopify.com/', steps: 'Create a custom app in your Shopify admin > Settings > Apps. Copy the Admin API access token.' },
22
- 'openai-models': { provider: 'OpenAI', url: 'https://platform.openai.com/api-keys', steps: 'Go to platform.openai.com > API Keys > Create new secret key. Copy it immediately — it won\'t be shown again.' },
23
- 'anthropic-models': { provider: 'Anthropic', url: 'https://console.anthropic.com/settings/keys', steps: 'Go to console.anthropic.com > Settings > API Keys > Create Key.' },
6
+ // ═══════════════════════════════════════════════════════════
7
+ // Skill Connections & MCP Hub — Enterprise Integration Center
8
+ // ═══════════════════════════════════════════════════════════
9
+
10
+ // Category display metadata
11
+ var CATEGORY_META = {
12
+ 'crm': { label: 'CRM & Sales', icon: 'users' },
13
+ 'communication': { label: 'Communication', icon: 'messages' },
14
+ 'productivity': { label: 'Productivity', icon: 'dashboard' },
15
+ 'devops': { label: 'DevOps & CI/CD', icon: 'code' },
16
+ 'finance': { label: 'Finance & Billing', icon: 'activity' },
17
+ 'marketing': { label: 'Marketing', icon: 'globe' },
18
+ 'hr': { label: 'HR & People', icon: 'users' },
19
+ 'ecommerce': { label: 'E-Commerce', icon: 'upload' },
20
+ 'infrastructure': { label: 'Infrastructure', icon: 'settings' },
21
+ 'design': { label: 'Design & Media', icon: 'journal' },
22
+ 'security': { label: 'Security', icon: 'shield' },
23
+ 'monitoring': { label: 'Monitoring', icon: 'activity' },
24
+ 'social': { label: 'Social Media', icon: 'globe' },
25
+ 'data-ai': { label: 'Data & AI', icon: 'code' },
26
+ 'enterprise': { label: 'Enterprise', icon: 'settings' },
27
+ 'cms': { label: 'CMS', icon: 'journal' },
28
+ 'general': { label: 'General', icon: 'settings' },
24
29
  };
25
30
 
26
- function getAuthHelp(skillId) {
27
- if (AUTH_HELP[skillId]) return AUTH_HELP[skillId];
28
- // Fallback: check if skill manifest has authHelp
29
- return null;
30
- }
31
+ var AUTH_TYPE_LABELS = {
32
+ 'oauth2': 'OAuth 2.0',
33
+ 'api_key': 'API Key',
34
+ 'token': 'Bearer Token',
35
+ 'credentials': 'Credentials',
36
+ };
31
37
 
32
- export function SkillConnectionsPage() {
33
- const { toast } = useApp();
34
- const [installed, setInstalled] = useState([]);
35
- const [statuses, setStatuses] = useState({});
36
- const [providers, setProviders] = useState([]);
37
- const [loading, setLoading] = useState(true);
38
- const [error, setError] = useState(null);
39
-
40
- // Config modal state
41
- const [configSkill, setConfigSkill] = useState(null);
42
- const [configSchema, setConfigSchema] = useState(null);
43
- const [configValues, setConfigValues] = useState({});
44
- const [configLoading, setConfigLoading] = useState(false);
45
- const [configSaving, setConfigSaving] = useState(false);
46
-
47
- // OAuth popup ref
48
- const [connectingSkillId, setConnectingSkillId] = useState(null);
49
-
50
- const loadStatuses = useCallback(function(skills) {
51
- var promises = skills.map(function(skill) {
52
- return engineCall('/oauth/status/' + skill.skillId)
53
- .then(function(d) { return { skillId: skill.skillId, status: d }; })
54
- .catch(function() { return { skillId: skill.skillId, status: { connected: false, provider: null, expiresAt: null } }; });
55
- });
56
- Promise.all(promises).then(function(results) {
57
- var map = {};
58
- results.forEach(function(r) { map[r.skillId] = r.status; });
59
- setStatuses(map);
60
- });
61
- }, []);
38
+ // ── Section 1: MCP Servers ──────────────────────────────
62
39
 
63
- const load = useCallback(function() {
64
- setLoading(true);
65
- setError(null);
40
+ function McpServersSection() {
41
+ var app = useApp(); var toast = app.toast;
42
+ var _servers = useState([]); var servers = _servers[0]; var setServers = _servers[1];
43
+ var _loading = useState(true); var loading = _loading[0]; var setLoading = _loading[1];
44
+ var _showAdd = useState(false); var showAdd = _showAdd[0]; var setShowAdd = _showAdd[1];
45
+ var _editServer = useState(null); var editServer = _editServer[0]; var setEditServer = _editServer[1];
46
+ var _testing = useState(null); var testing = _testing[0]; var setTesting = _testing[1];
66
47
 
67
- engineCall('/community/installed')
68
- .then(function(d) {
69
- var skills = d.installed || [];
70
- setInstalled(skills);
71
- loadStatuses(skills);
72
- })
73
- .catch(function(e) { setError(e.message || 'Failed to load installed skills'); })
74
- .finally(function() { setLoading(false); });
48
+ // Add/edit form
49
+ var _form = useState({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30 });
50
+ var form = _form[0]; var setForm = _form[1];
75
51
 
76
- engineCall('/oauth/providers')
77
- .then(function(d) { setProviders(d.providers || []); })
78
- .catch(function() {});
79
- }, [loadStatuses]);
52
+ var load = useCallback(function() {
53
+ setLoading(true);
54
+ engineCall('/mcp-servers')
55
+ .then(function(d) { setServers(d.servers || []); })
56
+ .catch(function() { setServers([]); })
57
+ .finally(function() { setLoading(false); });
58
+ }, []);
80
59
 
81
60
  useEffect(function() { load(); }, [load]);
82
61
 
83
- // Listen for OAuth popup messages
84
- useEffect(function() {
85
- function handleMessage(event) {
86
- if (event.data && event.data.type === 'oauth-result') {
87
- if (event.data.status === 'success') {
88
- toast('OAuth connected successfully', 'success');
89
- // Refresh status for the skill that was being connected
90
- if (connectingSkillId) {
91
- engineCall('/oauth/status/' + connectingSkillId)
92
- .then(function(d) {
93
- setStatuses(function(prev) {
94
- var updated = Object.assign({}, prev);
95
- updated[connectingSkillId] = d;
96
- return updated;
97
- });
98
- })
99
- .catch(function() {});
100
- }
101
- setConnectingSkillId(null);
102
- } else {
103
- toast(event.data.message || 'OAuth connection failed', 'error');
104
- setConnectingSkillId(null);
105
- }
106
- }
107
- }
108
- window.addEventListener('message', handleMessage);
109
- return function() { window.removeEventListener('message', handleMessage); };
110
- }, [connectingSkillId, toast]);
111
-
112
- // Connect OAuth
113
- var connectOAuth = async function(skillId) {
114
- setConnectingSkillId(skillId);
115
- try {
116
- var result = await engineCall('/oauth/authorize/' + skillId);
117
- if (result.authUrl) {
118
- var w = 600;
119
- var ht = 700;
120
- var left = (window.screen.width - w) / 2;
121
- var top = (window.screen.height - ht) / 2;
122
- window.open(
123
- result.authUrl,
124
- 'oauth_popup',
125
- 'width=' + w + ',height=' + ht + ',left=' + left + ',top=' + top + ',scrollbars=yes,resizable=yes'
126
- );
127
- } else {
128
- toast('No authorization URL returned', 'error');
129
- setConnectingSkillId(null);
130
- }
131
- } catch (e) {
132
- toast(e.message || 'Failed to start OAuth flow', 'error');
133
- setConnectingSkillId(null);
134
- }
62
+ var resetForm = function() {
63
+ setForm({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30 });
135
64
  };
136
65
 
137
- // Disconnect OAuth
138
- var disconnectOAuth = async function(skillId) {
139
- try {
140
- await engineCall('/oauth/disconnect/' + skillId, { method: 'DELETE' });
141
- toast('Disconnected successfully', 'success');
142
- setStatuses(function(prev) {
143
- var updated = Object.assign({}, prev);
144
- updated[skillId] = { connected: false, provider: null, expiresAt: null };
145
- return updated;
146
- });
147
- } catch (e) {
148
- toast(e.message || 'Disconnect failed', 'error');
149
- }
66
+ var openAdd = function() { resetForm(); setEditServer(null); setShowAdd(true); };
67
+
68
+ var openEdit = function(server) {
69
+ setForm({
70
+ name: server.name || '',
71
+ type: server.type || 'stdio',
72
+ command: server.command || '',
73
+ args: (server.args || []).join(' '),
74
+ url: server.url || '',
75
+ apiKey: server.apiKey || '',
76
+ headers: JSON.stringify(server.headers || {}, null, 2),
77
+ env: JSON.stringify(server.env || {}, null, 2),
78
+ enabled: server.enabled !== false,
79
+ description: server.description || '',
80
+ autoRestart: server.autoRestart !== false,
81
+ timeout: server.timeout || 30,
82
+ });
83
+ setEditServer(server);
84
+ setShowAdd(true);
150
85
  };
151
86
 
152
- // Open config modal
153
- var openConfig = async function(skill) {
154
- setConfigSkill(skill);
155
- setConfigLoading(true);
156
- setConfigSchema(null);
157
- setConfigValues(skill.config || {});
158
-
159
- try {
160
- var d = await engineCall('/community/skills/' + skill.skillId + '/config-schema');
161
- setConfigSchema(d.configSchema || {});
162
- } catch (e) {
163
- toast(e.message || 'Failed to load config schema', 'error');
164
- setConfigSchema({});
87
+ var saveServer = function() {
88
+ var payload = {
89
+ name: form.name.trim(),
90
+ type: form.type,
91
+ enabled: form.enabled,
92
+ description: form.description.trim(),
93
+ autoRestart: form.autoRestart,
94
+ timeout: parseInt(form.timeout) || 30,
95
+ };
96
+ if (form.type === 'stdio') {
97
+ payload.command = form.command.trim();
98
+ payload.args = form.args.trim().split(/\s+/).filter(Boolean);
99
+ try { payload.env = JSON.parse(form.env || '{}'); } catch { payload.env = {}; }
100
+ } else {
101
+ payload.url = form.url.trim();
102
+ if (form.apiKey) payload.apiKey = form.apiKey;
103
+ try { payload.headers = JSON.parse(form.headers || '{}'); } catch { payload.headers = {}; }
165
104
  }
166
- setConfigLoading(false);
167
- };
168
105
 
169
- // Save config
170
- var saveConfig = async function() {
171
- if (!configSkill) return;
172
- setConfigSaving(true);
173
- try {
174
- await engineCall('/community/skills/' + configSkill.skillId + '/config', {
175
- method: 'PUT',
176
- body: JSON.stringify(configValues)
177
- });
178
- toast('Configuration saved', 'success');
179
- // Update local installed list with new config
180
- setInstalled(function(prev) {
181
- return prev.map(function(s) {
182
- if (s.skillId === configSkill.skillId) {
183
- return Object.assign({}, s, { config: Object.assign({}, configValues) });
184
- }
185
- return s;
186
- });
187
- });
188
- setConfigSkill(null);
189
- } catch (e) {
190
- toast(e.message || 'Failed to save configuration', 'error');
191
- }
192
- setConfigSaving(false);
193
- };
106
+ if (!payload.name) { toast('Server name is required', 'error'); return; }
107
+ if (form.type === 'stdio' && !payload.command) { toast('Command is required for stdio servers', 'error'); return; }
108
+ if (form.type !== 'stdio' && !payload.url) { toast('URL is required for HTTP/SSE servers', 'error'); return; }
194
109
 
195
- // Helpers
196
- var providerMap = {};
197
- providers.forEach(function(p) { providerMap[p.id] = p; });
110
+ var method = editServer ? 'PUT' : 'POST';
111
+ var url = editServer ? '/mcp-servers/' + editServer.id : '/mcp-servers';
112
+ engineCall(url, { method: method, body: JSON.stringify(payload) })
113
+ .then(function(d) {
114
+ if (d.error) { toast(d.error, 'error'); return; }
115
+ toast(editServer ? 'Server updated' : 'Server added', 'success');
116
+ setShowAdd(false); load();
117
+ })
118
+ .catch(function(e) { toast(e.message, 'error'); });
119
+ };
198
120
 
199
- var getSkillStatus = function(skillId) {
200
- return statuses[skillId] || { connected: false, provider: null, expiresAt: null };
121
+ var deleteServer = function(id) {
122
+ if (!confirm('Remove this MCP server? Agents will lose access to its tools.')) return;
123
+ engineCall('/mcp-servers/' + id, { method: 'DELETE' })
124
+ .then(function() { toast('Server removed', 'success'); load(); })
125
+ .catch(function(e) { toast(e.message, 'error'); });
201
126
  };
202
127
 
203
- var hasOAuthProvider = function(skill) {
204
- var status = getSkillStatus(skill.skillId);
205
- return !!status.provider || providers.some(function(p) { return p.id === skill.skillId; });
128
+ var toggleServer = function(server) {
129
+ engineCall('/mcp-servers/' + server.id, { method: 'PUT', body: JSON.stringify({ enabled: !server.enabled }) })
130
+ .then(function() { toast(server.enabled ? 'Server disabled' : 'Server enabled', 'success'); load(); })
131
+ .catch(function(e) { toast(e.message, 'error'); });
206
132
  };
207
133
 
208
- var hasConfigFields = function(skill) {
209
- return skill.config && typeof skill.config === 'object' && Object.keys(skill.config).length > 0;
134
+ var testServer = function(server) {
135
+ setTesting(server.id);
136
+ engineCall('/mcp-servers/' + server.id + '/test', { method: 'POST' })
137
+ .then(function(d) {
138
+ if (d.error) toast('Connection failed: ' + d.error, 'error');
139
+ else toast('Connected! ' + (d.tools || 0) + ' tools discovered', 'success');
140
+ load();
141
+ })
142
+ .catch(function(e) { toast(e.message, 'error'); })
143
+ .finally(function() { setTesting(null); });
210
144
  };
211
145
 
212
- var needsConfig = function(skill) {
213
- var status = getSkillStatus(skill.skillId);
214
- return !status.connected && hasConfigFields(skill);
146
+ var typeLabel = function(t) {
147
+ if (t === 'stdio') return 'Local Process (stdio)';
148
+ if (t === 'sse') return 'Server-Sent Events (SSE)';
149
+ return 'HTTP (Streamable)';
215
150
  };
216
151
 
217
- // Computed stats
218
- var totalInstalled = installed.length;
219
- var connectedCount = installed.filter(function(s) { return getSkillStatus(s.skillId).connected; }).length;
220
- var needsConfigCount = installed.filter(function(s) { return needsConfig(s); }).length;
221
-
222
- // Status badge
223
- var statusBadge = function(skill) {
224
- var status = getSkillStatus(skill.skillId);
225
- if (status.connected) {
226
- return h('span', {
227
- className: 'badge',
228
- style: { background: 'var(--success)', color: '#fff', fontSize: 11 }
229
- }, 'Connected');
230
- }
231
- if (needsConfig(skill)) {
232
- return h('span', {
233
- className: 'badge',
234
- style: { background: 'var(--warning)', color: '#fff', fontSize: 11 }
235
- }, 'Needs Config');
236
- }
237
- return h('span', {
238
- className: 'badge',
239
- style: { background: 'var(--text-muted)', color: '#fff', fontSize: 11 }
240
- }, 'Not Connected');
152
+ var statusDot = function(server) {
153
+ var color = server.status === 'connected' ? 'var(--success)' : server.status === 'error' ? 'var(--danger)' : server.enabled ? 'var(--warning)' : 'var(--text-muted)';
154
+ return h('span', { style: { width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block', flexShrink: 0 } });
241
155
  };
242
156
 
243
- // Render config form field
244
- var renderConfigField = function(fieldName, schema) {
245
- var value = configValues[fieldName];
246
- var type = schema.type || 'string';
247
-
248
- if (type === 'boolean') {
249
- return h('div', { className: 'form-group', key: fieldName },
250
- h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' } },
251
- h('input', {
252
- type: 'checkbox',
253
- checked: !!value,
254
- onChange: function(e) {
255
- setConfigValues(function(prev) {
256
- var updated = Object.assign({}, prev);
257
- updated[fieldName] = e.target.checked;
258
- return updated;
259
- });
260
- }
261
- }),
262
- h('span', { className: 'form-label', style: { marginBottom: 0 } }, schema.label || fieldName)
263
- ),
264
- schema.description && h('p', { className: 'form-help' }, schema.description)
265
- );
266
- }
157
+ return h('div', null,
158
+ // Header
159
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } },
160
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
161
+ h('h2', { style: { fontSize: 16, fontWeight: 700, margin: 0 } }, 'MCP Servers'),
162
+ h(HelpButton, { label: 'MCP Servers' },
163
+ h('p', null, 'Connect external Model Context Protocol (MCP) servers to give your agents access to additional tools and capabilities.'),
164
+ h('h4', { style: { marginTop: 12, marginBottom: 6, fontSize: 14 } }, 'Connection Types'),
165
+ h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
166
+ h('li', null, h('strong', null, 'Local Process (stdio)'), ' — Runs a command on your server. The MCP server communicates via stdin/stdout. Best for locally installed tools.'),
167
+ h('li', null, h('strong', null, 'SSE (Server-Sent Events)'), ' — Connects to a remote MCP server via HTTP with SSE for streaming. Best for remote/cloud MCP servers.'),
168
+ h('li', null, h('strong', null, 'HTTP (Streamable)'), ' — Standard HTTP transport. Stateless request/response pattern.')
169
+ ),
170
+ h('h4', { style: { marginTop: 12, marginBottom: 6, fontSize: 14 } }, 'How it works'),
171
+ h('p', null, 'When you add an MCP server, we automatically discover all available tools it provides. These tools become available to your agents alongside the built-in integrations.'),
172
+ h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary)', borderRadius: 8, fontSize: 13 } },
173
+ h('strong', null, 'Examples: '),
174
+ 'npx @modelcontextprotocol/server-filesystem /path/to/dir, ',
175
+ 'npx @modelcontextprotocol/server-github, ',
176
+ 'docker run -i mcp/postgres, ',
177
+ 'Any MCP-compatible server'
178
+ )
179
+ )
180
+ ),
181
+ h('button', { className: 'btn btn-primary btn-sm', onClick: openAdd }, I.plus(), ' Add MCP Server')
182
+ ),
267
183
 
268
- if (type === 'select') {
269
- return h('div', { className: 'form-group', key: fieldName },
270
- h('label', { className: 'form-label' },
271
- schema.label || fieldName,
272
- schema.required && h('span', { style: { color: 'var(--danger)', marginLeft: 4 } }, '*')
184
+ // Server list
185
+ loading ? h('div', { style: { padding: 32, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'Loading MCP servers...')
186
+ : servers.length === 0 ? h('div', { style: { padding: 32, textAlign: 'center', border: '2px dashed var(--border)', borderRadius: 'var(--radius-lg)', color: 'var(--text-muted)' } },
187
+ h('div', { style: { marginBottom: 8 } }, I.code()),
188
+ h('p', { style: { fontSize: 14, fontWeight: 500, marginBottom: 4 } }, 'No MCP servers connected'),
189
+ h('p', { style: { fontSize: 12 } }, 'Add an MCP server to extend your agents with external tools'),
190
+ h('button', { className: 'btn btn-secondary btn-sm', style: { marginTop: 12 }, onClick: openAdd }, I.plus(), ' Add Your First Server')
191
+ )
192
+ : h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
193
+ servers.map(function(server) {
194
+ var isTesting = testing === server.id;
195
+ return h('div', { key: server.id, style: {
196
+ padding: '14px 16px', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
197
+ opacity: server.enabled === false ? 0.6 : 1,
198
+ } },
199
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
200
+ statusDot(server),
201
+ h('div', { style: { flex: 1, minWidth: 0 } },
202
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
203
+ h('span', { style: { fontWeight: 600, fontSize: 14 } }, server.name),
204
+ h('span', { className: 'badge badge-neutral', style: { fontSize: 10 } }, server.type === 'stdio' ? 'stdio' : server.type === 'sse' ? 'SSE' : 'HTTP'),
205
+ server.toolCount > 0 && h('span', { className: 'badge', style: { fontSize: 10, background: 'var(--accent-soft)', color: 'var(--accent)' } }, server.toolCount + ' tools'),
206
+ !server.enabled && h('span', { className: 'badge', style: { fontSize: 10, background: 'var(--bg-tertiary)', color: 'var(--text-muted)' } }, 'Disabled')
207
+ ),
208
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
209
+ server.type === 'stdio'
210
+ ? (server.command + ' ' + (server.args || []).join(' ')).trim()
211
+ : server.url || ''
212
+ ),
213
+ server.description && h('div', { style: { fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 } }, server.description)
214
+ ),
215
+ h('div', { style: { display: 'flex', gap: 4, flexShrink: 0 } },
216
+ h('button', { className: 'btn btn-ghost btn-sm', title: 'Test connection', disabled: isTesting, onClick: function() { testServer(server); } },
217
+ isTesting ? '...' : I.refresh()),
218
+ h('button', { className: 'btn btn-ghost btn-sm', title: server.enabled ? 'Disable' : 'Enable', onClick: function() { toggleServer(server); } },
219
+ server.enabled ? I.pause() : I.play()),
220
+ h('button', { className: 'btn btn-ghost btn-sm', title: 'Edit', onClick: function() { openEdit(server); } }, I.settings()),
221
+ h('button', { className: 'btn btn-ghost btn-sm', title: 'Remove', style: { color: 'var(--danger)' }, onClick: function() { deleteServer(server.id); } }, I.x())
222
+ )
223
+ ),
224
+ // Show tools if expanded (server has discovered tools)
225
+ server.tools && server.tools.length > 0 && h('div', { style: { marginTop: 10, paddingTop: 10, borderTop: '1px solid var(--border)', display: 'flex', flexWrap: 'wrap', gap: 4 } },
226
+ server.tools.slice(0, 20).map(function(tool) {
227
+ return h('span', { key: tool.name, style: {
228
+ fontSize: 10, padding: '2px 6px', borderRadius: 4,
229
+ background: 'var(--bg-tertiary)', color: 'var(--text-secondary)',
230
+ whiteSpace: 'nowrap',
231
+ } }, tool.name);
232
+ }),
233
+ server.tools.length > 20 && h('span', { style: { fontSize: 10, color: 'var(--text-muted)', padding: '2px 4px' } }, '+' + (server.tools.length - 20) + ' more')
234
+ )
235
+ );
236
+ })
237
+ ),
238
+
239
+ // Add/Edit modal
240
+ showAdd && h(Modal, {
241
+ title: editServer ? 'Edit MCP Server' : 'Add MCP Server',
242
+ onClose: function() { setShowAdd(false); },
243
+ footer: h(Fragment, null,
244
+ h('button', { className: 'btn btn-secondary', onClick: function() { setShowAdd(false); } }, 'Cancel'),
245
+ h('button', { className: 'btn btn-primary', onClick: saveServer }, editServer ? 'Save Changes' : 'Add Server')
246
+ )
247
+ },
248
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
249
+ // Name
250
+ h('div', { className: 'form-group' },
251
+ h('label', { className: 'form-label' }, 'Server Name *'),
252
+ h('input', { className: 'input', placeholder: 'e.g., GitHub MCP, Filesystem, Database', value: form.name,
253
+ onChange: function(e) { setForm(Object.assign({}, form, { name: e.target.value })); } })
273
254
  ),
274
- h('select', {
275
- className: 'input',
276
- value: value || schema.default || '',
277
- onChange: function(e) {
278
- setConfigValues(function(prev) {
279
- var updated = Object.assign({}, prev);
280
- updated[fieldName] = e.target.value;
281
- return updated;
282
- });
283
- }
284
- },
285
- h('option', { value: '' }, '-- Select --'),
286
- (schema.options || []).map(function(opt) {
287
- var optValue = typeof opt === 'string' ? opt : opt.value;
288
- var optLabel = typeof opt === 'string' ? opt : opt.label;
289
- return h('option', { key: optValue, value: optValue }, optLabel);
290
- })
255
+ // Description
256
+ h('div', { className: 'form-group' },
257
+ h('label', { className: 'form-label' }, 'Description'),
258
+ h('input', { className: 'input', placeholder: 'What does this server provide?', value: form.description,
259
+ onChange: function(e) { setForm(Object.assign({}, form, { description: e.target.value })); } })
291
260
  ),
292
- schema.description && h('p', { className: 'form-help' }, schema.description)
293
- );
294
- }
261
+ // Type selector
262
+ h('div', { className: 'form-group' },
263
+ h('label', { className: 'form-label' }, 'Connection Type *'),
264
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 } },
265
+ ['stdio', 'sse', 'http'].map(function(t) {
266
+ var selected = form.type === t;
267
+ return h('div', {
268
+ key: t,
269
+ onClick: function() { setForm(Object.assign({}, form, { type: t })); },
270
+ style: {
271
+ padding: '10px 12px', borderRadius: 'var(--radius)', cursor: 'pointer', textAlign: 'center',
272
+ border: '2px solid ' + (selected ? 'var(--accent)' : 'var(--border)'),
273
+ background: selected ? 'var(--accent-soft)' : 'var(--bg-secondary)',
274
+ }
275
+ },
276
+ h('div', { style: { fontWeight: 600, fontSize: 12 } }, t === 'stdio' ? 'Local Process' : t === 'sse' ? 'SSE' : 'HTTP'),
277
+ h('div', { style: { fontSize: 10, color: 'var(--text-muted)', marginTop: 2 } },
278
+ t === 'stdio' ? 'stdin/stdout' : t === 'sse' ? 'Server-Sent Events' : 'Streamable HTTP')
279
+ );
280
+ })
281
+ )
282
+ ),
283
+ // stdio fields
284
+ form.type === 'stdio' && h(Fragment, null,
285
+ h('div', { className: 'form-group' },
286
+ h('label', { className: 'form-label' }, 'Command *'),
287
+ h('input', { className: 'input', placeholder: 'npx, node, python, docker...', value: form.command,
288
+ onChange: function(e) { setForm(Object.assign({}, form, { command: e.target.value })); } }),
289
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, 'The executable to run. Must be installed on this machine.')
290
+ ),
291
+ h('div', { className: 'form-group' },
292
+ h('label', { className: 'form-label' }, 'Arguments'),
293
+ h('input', { className: 'input', placeholder: '@modelcontextprotocol/server-filesystem /home/user/docs', value: form.args,
294
+ onChange: function(e) { setForm(Object.assign({}, form, { args: e.target.value })); } }),
295
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, 'Space-separated arguments passed to the command.')
296
+ ),
297
+ h('div', { className: 'form-group' },
298
+ h('label', { className: 'form-label' }, 'Environment Variables'),
299
+ h('textarea', { className: 'input', rows: 3, placeholder: '{\n "GITHUB_TOKEN": "ghp_...",\n "DATABASE_URL": "postgres://..."\n}', value: form.env,
300
+ style: { fontFamily: 'var(--font-mono, monospace)', fontSize: 12 },
301
+ onChange: function(e) { setForm(Object.assign({}, form, { env: e.target.value })); } }),
302
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, 'JSON object of environment variables. Secrets are encrypted at rest.')
303
+ )
304
+ ),
305
+ // HTTP/SSE fields
306
+ form.type !== 'stdio' && h(Fragment, null,
307
+ h('div', { className: 'form-group' },
308
+ h('label', { className: 'form-label' }, 'Server URL *'),
309
+ h('input', { className: 'input', placeholder: form.type === 'sse' ? 'https://mcp.example.com/sse' : 'https://mcp.example.com/mcp', value: form.url,
310
+ onChange: function(e) { setForm(Object.assign({}, form, { url: e.target.value })); } })
311
+ ),
312
+ h('div', { className: 'form-group' },
313
+ h('label', { className: 'form-label' }, 'API Key / Bearer Token'),
314
+ h('input', { className: 'input', type: 'password', placeholder: 'Optional — for authenticated endpoints', value: form.apiKey,
315
+ onChange: function(e) { setForm(Object.assign({}, form, { apiKey: e.target.value })); } })
316
+ ),
317
+ h('div', { className: 'form-group' },
318
+ h('label', { className: 'form-label' }, 'Custom Headers'),
319
+ h('textarea', { className: 'input', rows: 2, placeholder: '{\n "X-Custom-Header": "value"\n}', value: form.headers,
320
+ style: { fontFamily: 'var(--font-mono, monospace)', fontSize: 12 },
321
+ onChange: function(e) { setForm(Object.assign({}, form, { headers: e.target.value })); } })
322
+ )
323
+ ),
324
+ // Common settings
325
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 } },
326
+ h('div', { className: 'form-group' },
327
+ h('label', { className: 'form-label' }, 'Connection Timeout (s)'),
328
+ h('input', { className: 'input', type: 'number', min: 5, max: 300, value: form.timeout,
329
+ onChange: function(e) { setForm(Object.assign({}, form, { timeout: e.target.value })); } })
330
+ ),
331
+ h('div', { className: 'form-group' },
332
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, marginTop: 24 } },
333
+ h('input', { type: 'checkbox', checked: form.autoRestart,
334
+ onChange: function(e) { setForm(Object.assign({}, form, { autoRestart: e.target.checked })); } }),
335
+ 'Auto-restart on failure'
336
+ )
337
+ )
338
+ ),
339
+ // Preset templates
340
+ !editServer && h('div', { style: { paddingTop: 12, borderTop: '1px solid var(--border)' } },
341
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 8 } }, 'Quick Start Templates'),
342
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
343
+ [
344
+ { label: 'Filesystem', name: 'Filesystem', cmd: 'npx', args: '-y @modelcontextprotocol/server-filesystem /home' },
345
+ { label: 'GitHub', name: 'GitHub', cmd: 'npx', args: '-y @modelcontextprotocol/server-github', envs: '{"GITHUB_PERSONAL_ACCESS_TOKEN": ""}' },
346
+ { label: 'PostgreSQL', name: 'PostgreSQL', cmd: 'npx', args: '-y @modelcontextprotocol/server-postgres', envs: '{"DATABASE_URL": ""}' },
347
+ { label: 'Brave Search', name: 'Brave Search', cmd: 'npx', args: '-y @modelcontextprotocol/server-brave-search', envs: '{"BRAVE_API_KEY": ""}' },
348
+ { label: 'Puppeteer', name: 'Puppeteer', cmd: 'npx', args: '-y @modelcontextprotocol/server-puppeteer' },
349
+ { label: 'Slack', name: 'Slack', cmd: 'npx', args: '-y @modelcontextprotocol/server-slack', envs: '{"SLACK_BOT_TOKEN": "", "SLACK_TEAM_ID": ""}' },
350
+ { label: 'Google Drive', name: 'Google Drive', cmd: 'npx', args: '-y @modelcontextprotocol/server-gdrive' },
351
+ { label: 'Memory', name: 'Memory', cmd: 'npx', args: '-y @modelcontextprotocol/server-memory' },
352
+ { label: 'Sentry', name: 'Sentry', cmd: 'npx', args: '-y @modelcontextprotocol/server-sentry', envs: '{"SENTRY_AUTH_TOKEN": ""}' },
353
+ { label: 'Fetch', name: 'Fetch', cmd: 'npx', args: '-y @modelcontextprotocol/server-fetch' },
354
+ ].map(function(tpl) {
355
+ return h('button', {
356
+ key: tpl.label,
357
+ className: 'btn btn-ghost btn-sm',
358
+ style: { fontSize: 11, padding: '4px 8px' },
359
+ onClick: function() { setForm(Object.assign({}, form, { name: tpl.name, type: 'stdio', command: tpl.cmd, args: tpl.args, env: tpl.envs || '{}' })); }
360
+ }, tpl.label);
361
+ })
362
+ )
363
+ )
364
+ )
365
+ )
366
+ );
367
+ }
295
368
 
296
- // Default: string or secret
297
- return h('div', { className: 'form-group', key: fieldName },
298
- h('label', { className: 'form-label' },
299
- schema.label || fieldName,
300
- schema.required && h('span', { style: { color: 'var(--danger)', marginLeft: 4 } }, '*')
301
- ),
302
- h('input', {
303
- className: 'input',
304
- type: type === 'secret' ? 'password' : 'text',
305
- value: value || '',
306
- placeholder: schema.placeholder || schema.default || '',
307
- onChange: function(e) {
308
- setConfigValues(function(prev) {
309
- var updated = Object.assign({}, prev);
310
- updated[fieldName] = e.target.value;
311
- return updated;
312
- });
313
- }
314
- }),
315
- schema.description && h('p', { className: 'form-help' }, schema.description)
316
- );
369
+ // ── Section 2: Built-in Integrations ─────────────────────
370
+
371
+ function IntegrationsSection() {
372
+ var app = useApp(); var toast = app.toast;
373
+ var _catalog = useState([]); var catalog = _catalog[0]; var setCatalog = _catalog[1];
374
+ var _categories = useState({}); var categories = _categories[0]; var setCategories = _categories[1];
375
+ var _loading = useState(true); var loading = _loading[0]; var setLoading = _loading[1];
376
+ var _search = useState(''); var search = _search[0]; var setSearch = _search[1];
377
+ var _catFilter = useState('all'); var catFilter = _catFilter[0]; var setCatFilter = _catFilter[1];
378
+ var _configModal = useState(null); var configModal = _configModal[0]; var setConfigModal = _configModal[1];
379
+ var _configValues = useState({}); var configValues = _configValues[0]; var setConfigValues = _configValues[1];
380
+ var _saving = useState(false); var saving = _saving[0]; var setSaving = _saving[1];
381
+ var _showAll = useState(false); var showAll = _showAll[0]; var setShowAll = _showAll[1];
382
+
383
+ var load = useCallback(function() {
384
+ setLoading(true);
385
+ engineCall('/integrations/catalog')
386
+ .then(function(d) { setCatalog(d.catalog || []); setCategories(d.categories || {}); })
387
+ .catch(function() {})
388
+ .finally(function() { setLoading(false); });
389
+ }, []);
390
+
391
+ useEffect(function() { load(); }, [load]);
392
+
393
+ // Filtering
394
+ var filtered = catalog.filter(function(item) {
395
+ if (catFilter !== 'all' && item.category !== catFilter) return false;
396
+ if (search) {
397
+ var q = search.toLowerCase();
398
+ return item.name.toLowerCase().includes(q) || item.skillId.toLowerCase().includes(q) || (item.category || '').toLowerCase().includes(q);
399
+ }
400
+ return true;
401
+ });
402
+
403
+ var connectedCount = catalog.filter(function(i) { return i.connected; }).length;
404
+ var cats = {};
405
+ catalog.forEach(function(i) { cats[i.category] = (cats[i.category] || 0) + 1; });
406
+
407
+ // Save credentials
408
+ var saveCredentials = function() {
409
+ if (!configModal) return;
410
+ setSaving(true);
411
+ engineCall('/integrations/' + configModal.skillId + '/credentials', {
412
+ method: 'PUT',
413
+ body: JSON.stringify(configValues)
414
+ })
415
+ .then(function(d) {
416
+ if (d.error) { toast(d.error, 'error'); }
417
+ else { toast(configModal.name + ' connected!', 'success'); setConfigModal(null); load(); }
418
+ })
419
+ .catch(function(e) { toast(e.message, 'error'); })
420
+ .finally(function() { setSaving(false); });
317
421
  };
318
422
 
319
- // Loading state
320
- if (loading) {
321
- return h('div', { style: { textAlign: 'center', padding: 60, color: 'var(--text-muted)' } }, 'Loading skill connections...');
322
- }
323
-
324
- // Error state
325
- if (error) {
326
- return h(Fragment, null,
327
- h('div', { style: { marginBottom: 20 } },
328
- h('h1', { style: { fontSize: 20, fontWeight: 700 } }, 'Skill Connections'),
329
- h('p', { style: { color: 'var(--text-muted)', fontSize: 13 } }, 'Connect external services and configure skill settings')
330
- ),
331
- h('div', { style: { textAlign: 'center', padding: 60 } },
332
- h('p', { style: { color: 'var(--danger)', marginBottom: 12 } }, error),
333
- h('button', { className: 'btn btn-primary', onClick: load }, I.refresh(), ' Retry')
334
- )
335
- );
336
- }
423
+ var disconnectIntegration = function(skillId) {
424
+ engineCall('/integrations/' + skillId + '/credentials', { method: 'DELETE' })
425
+ .then(function() { toast('Disconnected', 'success'); load(); })
426
+ .catch(function(e) { toast(e.message, 'error'); });
427
+ };
337
428
 
338
- var _h4 = { marginTop: 16, marginBottom: 8, fontSize: 14 };
339
- var _ul = { paddingLeft: 20, margin: '4px 0 8px' };
340
- var _tip = { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 };
429
+ var openConnect = function(item) {
430
+ if (item.authType === 'oauth2') {
431
+ // Start OAuth flow
432
+ engineCall('/oauth/authorize/' + item.skillId)
433
+ .then(function(d) {
434
+ if (d.authUrl) {
435
+ var w = 600, ht = 700;
436
+ window.open(d.authUrl, 'oauth_popup', 'width=' + w + ',height=' + ht + ',left=' + (screen.width - w) / 2 + ',top=' + (screen.height - ht) / 2);
437
+ }
438
+ })
439
+ .catch(function(e) { toast(e.message, 'error'); });
440
+ } else {
441
+ // Open credentials modal
442
+ var vals = {};
443
+ if (item.fields) item.fields.forEach(function(f) { vals[f] = ''; });
444
+ else vals.token = '';
445
+ setConfigValues(vals);
446
+ setConfigModal(item);
447
+ }
448
+ };
341
449
 
342
- return h(Fragment, null,
343
- // Page Header
344
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } },
345
- h('div', null,
346
- h('h1', { style: { fontSize: 20, fontWeight: 700, display: 'flex', alignItems: 'center' } }, 'Skill Connections', h(HelpButton, { label: 'Skill Connections' },
347
- h('p', null, 'Manage the authentication and configuration for your installed community skills. Each skill needs to be connected to its external service before agents can use it.'),
348
- h('h4', { style: _h4 }, 'Connection Types'),
349
- h('ul', { style: _ul },
350
- h('li', null, h('strong', null, 'OAuth'), ' Secure token-based auth. Click "Connect" to authorize via the service\'s login page.'),
351
- h('li', null, h('strong', null, 'API Key / Token'), ' Paste credentials from the service\'s developer portal.'),
352
- h('li', null, h('strong', null, 'Configure'), ' Set custom options like default channels, project IDs, or behavior tweaks.')
450
+ var displayItems = showAll ? filtered : filtered.slice(0, 24);
451
+
452
+ if (loading) return h('div', { style: { padding: 32, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'Loading integrations...');
453
+
454
+ return h('div', null,
455
+ // Header
456
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, flexWrap: 'wrap', gap: 8 } },
457
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
458
+ h('h2', { style: { fontSize: 16, fontWeight: 700, margin: 0 } }, 'Built-in Integrations'),
459
+ h('span', { className: 'badge badge-neutral', style: { fontSize: 10 } }, catalog.length),
460
+ h('span', { className: 'badge', style: { fontSize: 10, background: 'var(--success)', color: '#fff' } }, connectedCount + ' connected'),
461
+ h(HelpButton, { label: 'Built-in Integrations' },
462
+ h('p', null, 'AgenticMail includes ' + catalog.length + ' pre-built integrations powered by MCP adapters. Each integration provides tools that agents can use to interact with external services.'),
463
+ h('h4', { style: { marginTop: 12, marginBottom: 6, fontSize: 14 } }, 'Authentication Types'),
464
+ h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
465
+ h('li', null, h('strong', null, 'OAuth 2.0'), ' — Click "Connect" to authorize via the service\'s login page. Tokens auto-refresh.'),
466
+ h('li', null, h('strong', null, 'API Key'), ' — Paste a key from the service\'s developer settings.'),
467
+ h('li', null, h('strong', null, 'Credentials'), ' — Multiple fields (key + domain, key + project ID, etc.).')
353
468
  ),
354
- h('div', { style: _tip }, h('strong', null, 'Tip: '), 'Look for the "How to connect" box on each skill card — it links directly to where you get credentials.')
355
- )),
356
- h('p', { style: { color: 'var(--text-muted)', fontSize: 13 } }, 'Connect external services and configure skill settings')
469
+ h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary)', borderRadius: 8, fontSize: 13 } },
470
+ h('strong', null, 'Security: '), 'All credentials are encrypted in the vault. OAuth tokens auto-refresh before expiry.')
471
+ )
357
472
  ),
358
- h('button', { className: 'btn btn-secondary', onClick: load }, I.refresh(), ' Refresh')
473
+ h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
474
+ h('input', { className: 'input', placeholder: 'Search integrations...', value: search,
475
+ style: { width: 220, fontSize: 12 },
476
+ onChange: function(e) { setSearch(e.target.value); } }),
477
+ h('select', { className: 'input', value: catFilter, style: { width: 160, fontSize: 12 },
478
+ onChange: function(e) { setCatFilter(e.target.value); } },
479
+ h('option', { value: 'all' }, 'All categories (' + catalog.length + ')'),
480
+ Object.keys(cats).sort().map(function(cat) {
481
+ var meta = CATEGORY_META[cat] || { label: cat };
482
+ return h('option', { key: cat, value: cat }, meta.label + ' (' + cats[cat] + ')');
483
+ })
484
+ )
485
+ )
359
486
  ),
360
487
 
361
- // Stats Bar
362
- h('div', { className: 'stat-grid', style: { marginBottom: 20 } },
363
- h('div', { className: 'stat-card' },
364
- h('div', { className: 'stat-label', style: { display: 'flex', alignItems: 'center' } }, 'Total Installed', h(HelpButton, { label: 'Total Installed' }, h('p', null, 'The number of community skills you\'ve installed from the marketplace. These are available to connect and use with your agents.'))),
365
- h('div', { className: 'stat-value' }, totalInstalled)
366
- ),
367
- h('div', { className: 'stat-card' },
368
- h('div', { className: 'stat-label', style: { display: 'flex', alignItems: 'center' } }, 'Connected', h(HelpButton, { label: 'Connected' }, h('p', null, 'Skills that have been successfully authenticated with their external service. Connected skills are ready for agents to use.'))),
369
- h('div', { className: 'stat-value', style: { color: 'var(--success)' } }, connectedCount)
370
- ),
371
- h('div', { className: 'stat-card' },
372
- h('div', { className: 'stat-label', style: { display: 'flex', alignItems: 'center' } }, 'Needs Configuration', h(HelpButton, { label: 'Needs Configuration' }, h('p', null, 'Skills that are installed but not yet connected. They need API keys, OAuth authorization, or other credentials before agents can use them.'))),
373
- h('div', { className: 'stat-value', style: { color: needsConfigCount > 0 ? 'var(--warning)' : 'var(--text-muted)' } }, needsConfigCount)
374
- )
488
+ // Grid
489
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 8 } },
490
+ displayItems.map(function(item) {
491
+ var meta = CATEGORY_META[item.category] || {};
492
+ return h('div', { key: item.skillId, style: {
493
+ padding: '12px 14px', background: 'var(--bg-card)', border: '1px solid ' + (item.connected ? 'rgba(21,128,61,0.3)' : 'var(--border)'),
494
+ borderRadius: 'var(--radius)', display: 'flex', alignItems: 'center', gap: 10,
495
+ } },
496
+ h('div', { style: { flex: 1, minWidth: 0 } },
497
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
498
+ h('span', { style: { fontWeight: 600, fontSize: 13 } }, item.name),
499
+ item.connected && h('span', { style: { width: 6, height: 6, borderRadius: '50%', background: 'var(--success)', flexShrink: 0 } })
500
+ ),
501
+ h('div', { style: { fontSize: 10, color: 'var(--text-muted)', marginTop: 2, display: 'flex', gap: 6 } },
502
+ h('span', null, (meta.label || item.category)),
503
+ h('span', null, '\u00B7'),
504
+ h('span', null, AUTH_TYPE_LABELS[item.authType] || item.authType),
505
+ h('span', null, '\u00B7'),
506
+ h('span', null, item.toolCount + ' tools')
507
+ )
508
+ ),
509
+ item.connected
510
+ ? h('button', { className: 'btn btn-ghost btn-sm', style: { fontSize: 11, color: 'var(--danger)', flexShrink: 0 },
511
+ onClick: function() { disconnectIntegration(item.skillId); } }, 'Disconnect')
512
+ : h('button', { className: 'btn btn-primary btn-sm', style: { fontSize: 11, flexShrink: 0 },
513
+ onClick: function() { openConnect(item); } }, 'Connect')
514
+ );
515
+ })
375
516
  ),
376
517
 
377
- // Empty state
378
- installed.length === 0 && h('div', { style: { textAlign: 'center', padding: 60, color: 'var(--text-muted)' } },
379
- h('div', { style: { fontSize: 48, marginBottom: 16 } }, '\uD83D\uDD17'),
380
- h('p', { style: { fontSize: 15, fontWeight: 500, marginBottom: 8 } }, 'No community skills installed'),
381
- h('p', { style: { fontSize: 13 } }, 'Install skills from the Community Marketplace to manage their connections here.')
518
+ // Show more
519
+ filtered.length > 24 && !showAll && h('div', { style: { textAlign: 'center', marginTop: 12 } },
520
+ h('button', { className: 'btn btn-secondary btn-sm', onClick: function() { setShowAll(true); } },
521
+ 'Show all ' + filtered.length + ' integrations')
382
522
  ),
383
523
 
384
- // Skill Cards Grid
385
- installed.length > 0 && h('div', {
386
- style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 16 }
524
+ // Credentials modal
525
+ configModal && h(Modal, {
526
+ title: 'Connect ' + configModal.name,
527
+ onClose: function() { setConfigModal(null); },
528
+ footer: h(Fragment, null,
529
+ h('button', { className: 'btn btn-secondary', onClick: function() { setConfigModal(null); } }, 'Cancel'),
530
+ h('button', { className: 'btn btn-primary', disabled: saving, onClick: saveCredentials }, saving ? 'Saving...' : 'Connect')
531
+ )
387
532
  },
388
- installed.map(function(skill) {
389
- var status = getSkillStatus(skill.skillId);
390
- var meta = skill.skill || skill.manifest || skill;
391
- var skillName = meta.name || skill.skillId;
392
- var skillDesc = meta.description || '';
393
- var isConnecting = connectingSkillId === skill.skillId;
394
-
395
- return h('div', { key: skill.skillId, className: 'card', style: { padding: 20 } },
396
- // Card header: name + status badge
397
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 } },
398
- h('div', { style: { flex: 1, minWidth: 0 } },
399
- h('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 4 } }, skillName),
400
- skillDesc && h('div', { style: { fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.4 } }, skillDesc)
401
- ),
402
- h('div', { style: { marginLeft: 12, flexShrink: 0 } }, statusBadge(skill))
403
- ),
533
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
534
+ h('p', { style: { fontSize: 13, color: 'var(--text-secondary)' } },
535
+ 'Enter the credentials for ', h('strong', null, configModal.name), '. All values are encrypted in the vault.'
536
+ ),
537
+ configModal.fields ? configModal.fields.map(function(field) {
538
+ var label = (configModal.fieldLabels || {})[field] || field;
539
+ var isSecret = /key|token|secret|password/i.test(field);
540
+ return h('div', { className: 'form-group', key: field },
541
+ h('label', { className: 'form-label' }, label),
542
+ h('input', {
543
+ className: 'input',
544
+ type: isSecret ? 'password' : 'text',
545
+ value: configValues[field] || '',
546
+ placeholder: label,
547
+ onChange: function(e) {
548
+ setConfigValues(function(prev) { var u = Object.assign({}, prev); u[field] = e.target.value; return u; });
549
+ }
550
+ })
551
+ );
552
+ })
553
+ : h('div', { className: 'form-group' },
554
+ h('label', { className: 'form-label' }, configModal.authType === 'api_key' ? 'API Key' : 'Access Token'),
555
+ h('input', {
556
+ className: 'input', type: 'password',
557
+ value: configValues.token || '',
558
+ placeholder: 'Paste your ' + (configModal.authType === 'api_key' ? 'API key' : 'access token'),
559
+ onChange: function(e) { setConfigValues({ token: e.target.value }); }
560
+ })
561
+ )
562
+ )
563
+ )
564
+ );
565
+ }
404
566
 
405
- // Connection info
406
- status.connected && status.provider && h('div', {
407
- style: { fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12, padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 6 }
408
- },
409
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
410
- h('span', null, 'Provider: ', h('strong', null, status.provider)),
411
- status.expiresAt && h('span', { style: { color: 'var(--text-muted)', fontSize: 11 } },
412
- 'Expires: ' + new Date(status.expiresAt).toLocaleDateString()
413
- )
414
- )
415
- ),
567
+ // ── Section 3: Community Skills ──────────────────────────
416
568
 
417
- // Auth help info (show when not connected)
418
- !status.connected && (function() {
419
- var help = getAuthHelp(skill.skillId) || (meta.authHelp ? meta.authHelp : null);
420
- if (!help) return null;
421
- return h('div', {
422
- style: { fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12, padding: '10px 12px', background: 'var(--bg-tertiary)', borderRadius: 6, border: '1px solid var(--border)' }
423
- },
424
- h('div', { style: { fontWeight: 600, marginBottom: 4, fontSize: 11, textTransform: 'uppercase', color: 'var(--primary)' } }, 'How to connect'),
425
- h('div', { style: { lineHeight: 1.5, marginBottom: 6 } }, help.steps || help.description),
426
- help.url && h('a', { href: help.url, target: '_blank', rel: 'noopener', style: { color: 'var(--primary)', fontSize: 12, textDecoration: 'none' } }, 'Get credentials at ' + (help.provider || 'provider') + ' \u2192')
427
- );
428
- })(),
429
-
430
- // Action buttons
431
- h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 'auto' } },
432
- // Connect / Disconnect
433
- status.connected
434
- ? h('button', {
435
- className: 'btn btn-danger btn-sm',
436
- onClick: function() { disconnectOAuth(skill.skillId); }
437
- }, 'Disconnect')
438
- : hasOAuthProvider(skill) && h('button', {
439
- className: 'btn btn-primary btn-sm',
440
- disabled: isConnecting,
441
- onClick: function() { connectOAuth(skill.skillId); }
442
- }, isConnecting ? 'Connecting...' : 'Connect'),
443
-
444
- // Configure button (always available if skill has config potential)
445
- h('button', {
446
- className: 'btn btn-secondary btn-sm',
447
- onClick: function() { openConfig(skill); }
448
- }, I.settings(), ' Configure')
449
- )
450
- );
569
+ function CommunitySkillsSection() {
570
+ var app = useApp(); var toast = app.toast;
571
+ var _installed = useState([]); var installed = _installed[0]; var setInstalled = _installed[1];
572
+ var _statuses = useState({}); var statuses = _statuses[0]; var setStatuses = _statuses[1];
573
+ var _loading = useState(true); var loading = _loading[0]; var setLoading = _loading[1];
574
+ var _configSkill = useState(null); var configSkill = _configSkill[0]; var setConfigSkill = _configSkill[1];
575
+ var _configSchema = useState(null); var configSchema = _configSchema[0]; var setConfigSchema = _configSchema[1];
576
+ var _configValues = useState({}); var configValues = _configValues[0]; var setConfigValues = _configValues[1];
577
+ var _configSaving = useState(false); var configSaving = _configSaving[0]; var setConfigSaving = _configSaving[1];
578
+
579
+ var load = useCallback(function() {
580
+ setLoading(true);
581
+ engineCall('/community/installed')
582
+ .then(function(d) {
583
+ var skills = d.installed || [];
584
+ setInstalled(skills);
585
+ // Load statuses
586
+ var promises = skills.map(function(skill) {
587
+ return engineCall('/oauth/status/' + skill.skillId)
588
+ .then(function(d) { return { skillId: skill.skillId, status: d }; })
589
+ .catch(function() { return { skillId: skill.skillId, status: { connected: false } }; });
590
+ });
591
+ Promise.all(promises).then(function(results) {
592
+ var map = {};
593
+ results.forEach(function(r) { map[r.skillId] = r.status; });
594
+ setStatuses(map);
595
+ });
451
596
  })
597
+ .catch(function() {})
598
+ .finally(function() { setLoading(false); });
599
+ }, []);
600
+
601
+ useEffect(function() { load(); }, [load]);
602
+
603
+ var openConfig = function(skill) {
604
+ setConfigSkill(skill);
605
+ setConfigValues(skill.config || {});
606
+ engineCall('/community/skills/' + skill.skillId + '/config-schema')
607
+ .then(function(d) { setConfigSchema(d.configSchema || {}); })
608
+ .catch(function() { setConfigSchema({}); });
609
+ };
610
+
611
+ var saveConfig = function() {
612
+ if (!configSkill) return;
613
+ setConfigSaving(true);
614
+ engineCall('/community/skills/' + configSkill.skillId + '/config', { method: 'PUT', body: JSON.stringify(configValues) })
615
+ .then(function() { toast('Saved', 'success'); setConfigSkill(null); load(); })
616
+ .catch(function(e) { toast(e.message, 'error'); })
617
+ .finally(function() { setConfigSaving(false); });
618
+ };
619
+
620
+ if (loading) return h('div', { style: { padding: 32, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'Loading community skills...');
621
+
622
+ return h('div', null,
623
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } },
624
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
625
+ h('h2', { style: { fontSize: 16, fontWeight: 700, margin: 0 } }, 'Community Skills'),
626
+ h('span', { className: 'badge badge-neutral', style: { fontSize: 10 } }, installed.length),
627
+ h(HelpButton, { label: 'Community Skills' },
628
+ h('p', null, 'Skills installed from the Community Marketplace. These are custom skill packages that add specialized capabilities to your agents.'),
629
+ h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary)', borderRadius: 8, fontSize: 13 } },
630
+ h('strong', null, 'Tip: '), 'Install more skills from the Community Skills page in the sidebar.')
631
+ )
632
+ ),
633
+ h('button', { className: 'btn btn-secondary btn-sm', onClick: load }, I.refresh())
452
634
  ),
453
635
 
454
- // Config Modal
636
+ installed.length === 0
637
+ ? h('div', { style: { padding: 32, textAlign: 'center', border: '2px dashed var(--border)', borderRadius: 'var(--radius-lg)', color: 'var(--text-muted)' } },
638
+ h('p', { style: { fontSize: 13 } }, 'No community skills installed. Visit the marketplace to add some.')
639
+ )
640
+ : h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 8 } },
641
+ installed.map(function(skill) {
642
+ var status = statuses[skill.skillId] || {};
643
+ var meta = skill.skill || skill.manifest || skill;
644
+ return h('div', { key: skill.skillId, style: {
645
+ padding: '12px 14px', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
646
+ display: 'flex', alignItems: 'center', gap: 10,
647
+ } },
648
+ h('div', { style: { flex: 1, minWidth: 0 } },
649
+ h('div', { style: { fontWeight: 600, fontSize: 13 } }, meta.name || skill.skillId),
650
+ meta.description && h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 } }, meta.description)
651
+ ),
652
+ h('div', { style: { display: 'flex', gap: 4, flexShrink: 0 } },
653
+ status.connected
654
+ ? h('span', { className: 'badge', style: { background: 'var(--success)', color: '#fff', fontSize: 10 } }, 'Connected')
655
+ : h('span', { className: 'badge badge-neutral', style: { fontSize: 10 } }, 'Not connected'),
656
+ h('button', { className: 'btn btn-ghost btn-sm', onClick: function() { openConfig(skill); } }, I.settings())
657
+ )
658
+ );
659
+ })
660
+ ),
661
+
662
+ // Config modal
455
663
  configSkill && h(Modal, {
456
- title: 'Configure ' + (configSkill.skill?.name || configSkill.manifest?.name || configSkill.skillId),
664
+ title: 'Configure ' + (configSkill.skill?.name || configSkill.skillId),
457
665
  onClose: function() { setConfigSkill(null); },
458
666
  footer: h(Fragment, null,
459
667
  h('button', { className: 'btn btn-secondary', onClick: function() { setConfigSkill(null); } }, 'Cancel'),
460
- h('button', {
461
- className: 'btn btn-primary',
462
- onClick: saveConfig,
463
- disabled: configSaving || configLoading
464
- }, configSaving ? 'Saving...' : 'Save Configuration')
668
+ h('button', { className: 'btn btn-primary', disabled: configSaving, onClick: saveConfig }, configSaving ? 'Saving...' : 'Save')
465
669
  )
466
670
  },
467
- configLoading
468
- ? h('div', { style: { textAlign: 'center', padding: 24, color: 'var(--text-muted)' } }, 'Loading configuration schema...')
469
- : configSchema && Object.keys(configSchema).length > 0
470
- ? h('div', null,
471
- // Show auth help in modal too
472
- (function() {
473
- var help = getAuthHelp(configSkill.skillId) || (configSkill.skill?.authHelp || configSkill.manifest?.authHelp);
474
- if (!help) return null;
475
- return h('div', {
476
- style: { marginBottom: 16, padding: '12px 14px', background: 'var(--bg-tertiary)', borderRadius: 8, border: '1px solid var(--border)' }
477
- },
478
- h('div', { style: { fontWeight: 600, marginBottom: 4, fontSize: 12, color: 'var(--primary)' } }, 'Where to get credentials'),
479
- h('div', { style: { fontSize: 13, lineHeight: 1.5, color: 'var(--text-secondary)', marginBottom: 6 } }, help.steps || help.description),
480
- help.url && h('a', { href: help.url, target: '_blank', rel: 'noopener', style: { color: 'var(--primary)', fontSize: 13 } }, 'Open ' + (help.provider || 'provider') + ' developer portal \u2192')
481
- );
482
- })(),
483
- h('p', { style: { fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 } },
484
- 'Configure the settings for this skill. Fields marked with * are required.'
485
- ),
486
- Object.entries(configSchema).map(function(entry) {
487
- return renderConfigField(entry[0], entry[1]);
488
- })
489
- )
490
- : h('div', null,
491
- h('p', { style: { fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 } },
492
- 'This skill accepts custom configuration. Enter key-value pairs below.'
493
- ),
494
- // Fallback: show existing config as editable key-value pairs
495
- Object.entries(configValues).map(function(entry) {
496
- return h('div', { className: 'form-group', key: entry[0] },
497
- h('label', { className: 'form-label' }, entry[0]),
498
- h('input', {
499
- className: 'input',
500
- value: entry[1] || '',
501
- onChange: function(e) {
502
- setConfigValues(function(prev) {
503
- var updated = Object.assign({}, prev);
504
- updated[entry[0]] = e.target.value;
505
- return updated;
506
- });
507
- }
508
- })
509
- );
510
- }),
511
- Object.keys(configValues).length === 0 && h('div', {
512
- style: { textAlign: 'center', padding: 20, color: 'var(--text-muted)' }
513
- }, 'No configuration schema available for this skill.')
514
- )
671
+ configSchema && Object.keys(configSchema).length > 0
672
+ ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
673
+ Object.entries(configSchema).map(function(entry) {
674
+ var fieldName = entry[0]; var schema = entry[1];
675
+ var isSecret = schema.type === 'secret' || /key|token|secret/i.test(fieldName);
676
+ return h('div', { className: 'form-group', key: fieldName },
677
+ h('label', { className: 'form-label' }, schema.label || fieldName),
678
+ h('input', {
679
+ className: 'input',
680
+ type: isSecret ? 'password' : 'text',
681
+ value: configValues[fieldName] || '',
682
+ placeholder: schema.placeholder || schema.default || '',
683
+ onChange: function(e) { setConfigValues(function(p) { var u = Object.assign({}, p); u[fieldName] = e.target.value; return u; }); }
684
+ }),
685
+ schema.description && h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, schema.description)
686
+ );
687
+ })
688
+ )
689
+ : h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)' } }, 'No configuration options available.')
515
690
  )
516
691
  );
517
692
  }
693
+
694
+ // ═══════════════════════════════════════════════════════════
695
+ // Main Page
696
+ // ═══════════════════════════════════════════════════════════
697
+
698
+ export function SkillConnectionsPage() {
699
+ var _tab = useState('mcp'); var tab = _tab[0]; var setTab = _tab[1];
700
+
701
+ var _h4 = { marginTop: 16, marginBottom: 8, fontSize: 14 };
702
+ var _tip = { marginTop: 12, padding: 12, background: 'var(--bg-secondary)', borderRadius: 8, fontSize: 13 };
703
+
704
+ return h(Fragment, null,
705
+ // Page Header
706
+ h('div', { style: { marginBottom: 20 } },
707
+ h('h1', { style: { fontSize: 20, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 } },
708
+ 'Integrations & MCP Hub',
709
+ h(HelpButton, { label: 'Integrations & MCP Hub' },
710
+ h('p', null, 'The central hub for connecting your AI agents to external tools, services, and MCP servers. Everything your agents need to interact with the outside world is managed here.'),
711
+ h('h4', { style: _h4 }, 'Three connection types'),
712
+ h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
713
+ h('li', null, h('strong', null, 'MCP Servers'), ' — Connect external Model Context Protocol servers (like Claude Code\'s MCP system). Agents get access to all tools the server provides.'),
714
+ h('li', null, h('strong', null, 'Built-in Integrations'), ' — 145+ pre-built adapters for popular services (Slack, GitHub, Salesforce, etc.). Just add credentials.'),
715
+ h('li', null, h('strong', null, 'Community Skills'), ' — Custom skill packages from the marketplace. Install and configure them here.')
716
+ ),
717
+ h('div', { style: _tip }, h('strong', null, 'MCP Protocol: '), 'MCP (Model Context Protocol) is an open standard by Anthropic for connecting AI models to external tools. Any MCP-compatible server works here — same format as Claude Code, Cursor, and other AI tools.')
718
+ )
719
+ ),
720
+ h('p', { style: { color: 'var(--text-muted)', fontSize: 13, marginTop: 4 } },
721
+ 'Connect MCP servers, built-in integrations, and community skills to extend your agents')
722
+ ),
723
+
724
+ // Tab bar
725
+ h('div', { className: 'tabs', style: { marginBottom: 20 } },
726
+ h('div', { className: 'tab' + (tab === 'mcp' ? ' active' : ''), onClick: function() { setTab('mcp'); } }, I.code(), ' MCP Servers'),
727
+ h('div', { className: 'tab' + (tab === 'integrations' ? ' active' : ''), onClick: function() { setTab('integrations'); } }, I.globe(), ' Built-in Integrations'),
728
+ h('div', { className: 'tab' + (tab === 'community' ? ' active' : ''), onClick: function() { setTab('community'); } }, I.users(), ' Community Skills')
729
+ ),
730
+
731
+ // Tab content
732
+ tab === 'mcp' && h(McpServersSection),
733
+ tab === 'integrations' && h(IntegrationsSection),
734
+ tab === 'community' && h(CommunitySkillsSection)
735
+ );
736
+ }