apteva 0.4.19 → 0.4.20
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/dist/ActivityPage.h769ek3a.js +3 -0
- package/dist/{ApiDocsPage.rfpf7ws1.js → ApiDocsPage.kf6bbwkk.js} +1 -1
- package/dist/{App.7vzbaz56.js → App.039re6cf.js} +1 -1
- package/dist/{App.amwp54wf.js → App.2jmkqm8c.js} +1 -1
- package/dist/{App.p93mmyqw.js → App.2yy66bnp.js} +1 -1
- package/dist/{App.f8qsyhpr.js → App.3515wsb4.js} +1 -1
- package/dist/{App.6nc5acvk.js → App.7v1w3ys9.js} +1 -1
- package/dist/{App.sdsc0258.js → App.c90t3dxg.js} +1 -1
- package/dist/{App.5qw2dtxs.js → App.edwahsvz.js} +1 -1
- package/dist/{App.qmg33p02.js → App.jfx3der4.js} +1 -1
- package/dist/{App.kfyrnznw.js → App.n4jb3c22.js} +1 -1
- package/dist/App.q3bpx15d.js +20 -0
- package/dist/{App.e4202qb4.js → App.r0a2nmqs.js} +84 -84
- package/dist/{App.8rfz30p1.js → App.s2yrcz15.js} +1 -1
- package/dist/{App.1nmg2h01.js → App.s5j82a5j.js} +1 -1
- package/dist/{App.errxz2q4.js → App.tg1b94tx.js} +1 -1
- package/dist/ConnectionsPage.a67fjgbf.js +3 -0
- package/dist/McpPage.d4p3xvtk.js +3 -0
- package/dist/SettingsPage.46sqpe39.js +3 -0
- package/dist/SkillsPage.j9hkqm99.js +3 -0
- package/dist/TasksPage.6pvkb7s7.js +3 -0
- package/dist/TelemetryPage.5zq9msb5.js +3 -0
- package/dist/TestsPage.24432yqt.js +3 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/mcp-platform.ts +353 -2
- package/src/routes/api/agents.ts +15 -2
- package/src/routes/api/system.ts +12 -1
- package/src/routes/auth.ts +11 -2
- package/src/web/App.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +5 -4
- package/src/web/context/AuthContext.tsx +18 -11
- package/src/web/hooks/useAgents.ts +7 -3
- package/src/web/hooks/useOnboarding.ts +9 -30
- package/dist/ActivityPage.9a1qg4bp.js +0 -3
- package/dist/App.g8vq68n0.js +0 -20
- package/dist/ConnectionsPage.7zqba1r0.js +0 -3
- package/dist/McpPage.kf2g327t.js +0 -3
- package/dist/SettingsPage.472c15ep.js +0 -3
- package/dist/SkillsPage.xdxnh68a.js +0 -3
- package/dist/TasksPage.7g0b8xwc.js +0 -3
- package/dist/TelemetryPage.pr7rbz4r.js +0 -3
- package/dist/TestsPage.zhc6rqjm.js +0 -3
package/src/mcp-platform.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
// Built-in MCP server that exposes the Apteva platform API as MCP tools
|
|
2
2
|
// This allows the meta agent (Apteva Assistant) to control the platform
|
|
3
3
|
|
|
4
|
-
import { AgentDB, ProjectDB, McpServerDB, SkillDB, TelemetryDB, generateId } from "./db";
|
|
4
|
+
import { AgentDB, ProjectDB, McpServerDB, SkillDB, TelemetryDB, SubscriptionDB, SettingsDB, generateId } from "./db";
|
|
5
5
|
import { TestCaseDB, TestRunDB } from "./db-tests";
|
|
6
6
|
import { runTest, runAll } from "./test-runner";
|
|
7
|
-
import { getProvidersWithStatus, PROVIDERS } from "./providers";
|
|
7
|
+
import { getProvidersWithStatus, PROVIDERS, ProviderKeys } from "./providers";
|
|
8
8
|
import { startAgentProcess, setAgentStatus, toApiAgent, META_AGENT_ID, agentFetch } from "./routes/api/agent-utils";
|
|
9
9
|
import { agentProcesses } from "./server";
|
|
10
|
+
import { getTriggerProvider, getTriggerProviderIds, registerTriggerProvider } from "./triggers";
|
|
11
|
+
import { ComposioTriggerProvider } from "./triggers/composio";
|
|
12
|
+
import { AgentDojoTriggerProvider } from "./triggers/agentdojo";
|
|
13
|
+
import { getProvider, registerProvider } from "./integrations";
|
|
14
|
+
import { ComposioProvider } from "./integrations/composio";
|
|
15
|
+
import { AgentDojoProvider } from "./integrations/agentdojo";
|
|
16
|
+
|
|
17
|
+
// Register trigger + integration providers on module load
|
|
18
|
+
registerTriggerProvider(ComposioTriggerProvider);
|
|
19
|
+
registerTriggerProvider(AgentDojoTriggerProvider);
|
|
20
|
+
registerProvider(ComposioProvider);
|
|
21
|
+
registerProvider(AgentDojoProvider);
|
|
10
22
|
|
|
11
23
|
// MCP Protocol version
|
|
12
24
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -447,6 +459,128 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
|
|
|
447
459
|
required: ["test_id"],
|
|
448
460
|
},
|
|
449
461
|
},
|
|
462
|
+
// Subscription & Trigger management
|
|
463
|
+
{
|
|
464
|
+
name: "list_trigger_providers",
|
|
465
|
+
description: "List available trigger/webhook providers (e.g. composio, agentdojo) and whether they have API keys configured.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "list_trigger_types",
|
|
475
|
+
description: `Browse available trigger types from a provider. Trigger types are events you can subscribe to (e.g. github:push, stripe:payment_intent, slack:message).
|
|
476
|
+
|
|
477
|
+
Each trigger type has:
|
|
478
|
+
- slug: unique identifier (e.g. "github:push")
|
|
479
|
+
- name: display name
|
|
480
|
+
- description: what the trigger does
|
|
481
|
+
- config_schema: JSON schema of required config fields (e.g. owner, repo for GitHub triggers)
|
|
482
|
+
|
|
483
|
+
Use this to find trigger slugs before creating a subscription.`,
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: "object",
|
|
486
|
+
properties: {
|
|
487
|
+
provider: { type: "string", description: "Trigger provider ID: composio or agentdojo" },
|
|
488
|
+
project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
|
|
489
|
+
},
|
|
490
|
+
required: ["provider"],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "list_connected_accounts",
|
|
495
|
+
description: "List connected accounts (OAuth connections) for a provider. You need a connected_account_id to create a subscription. Each account represents a user's authenticated connection to a service (e.g. GitHub, Slack, Stripe).",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
provider: { type: "string", description: "Integration provider ID: composio or agentdojo" },
|
|
500
|
+
project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
|
|
501
|
+
},
|
|
502
|
+
required: ["provider"],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "list_subscriptions",
|
|
507
|
+
description: "List local trigger subscriptions. Subscriptions route incoming webhook events to agents. Each subscription maps a trigger (e.g. github:push) to a specific agent.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
agent_id: { type: "string", description: "Filter by agent ID (optional)" },
|
|
512
|
+
project_id: { type: "string", description: "Filter by project ID (optional)" },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "create_subscription",
|
|
518
|
+
description: `Create a trigger subscription: registers a webhook with the external service (via the provider) and creates a local subscription to route events to an agent.
|
|
519
|
+
|
|
520
|
+
WORKFLOW:
|
|
521
|
+
1. Use list_trigger_providers to check which providers have keys
|
|
522
|
+
2. Use list_trigger_types to find the trigger slug you want
|
|
523
|
+
3. Use list_connected_accounts to find the connected_account_id
|
|
524
|
+
4. Use list_agents to find the agent_id to route events to
|
|
525
|
+
5. Call create_subscription with all the above
|
|
526
|
+
|
|
527
|
+
IMPORTANT: Some triggers require extra config fields (e.g. GitHub triggers need "owner" and "repo"). Check the trigger type's config_schema and pass required fields in the config object.`,
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: "object",
|
|
530
|
+
properties: {
|
|
531
|
+
provider: { type: "string", description: "Trigger provider ID: composio or agentdojo" },
|
|
532
|
+
trigger_slug: { type: "string", description: "Trigger type slug (e.g. 'github:push', 'GITHUB_PUSH_EVENT'). Use list_trigger_types to find slugs." },
|
|
533
|
+
connected_account_id: { type: "string", description: "Connected account ID that owns the integration. Use list_connected_accounts to find IDs." },
|
|
534
|
+
agent_id: { type: "string", description: "Agent ID to route trigger events to. Use list_agents to find IDs." },
|
|
535
|
+
project_id: { type: "string", description: "Project ID for scoping (optional)" },
|
|
536
|
+
config: {
|
|
537
|
+
type: "object",
|
|
538
|
+
description: "Extra config fields required by the trigger type (e.g. { owner: 'myorg', repo: 'myrepo' } for GitHub). Check config_schema from list_trigger_types.",
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
required: ["provider", "trigger_slug", "connected_account_id", "agent_id"],
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "enable_subscription",
|
|
546
|
+
description: "Enable a disabled subscription so it starts routing events to the agent again. Optionally also enables the remote trigger on the provider.",
|
|
547
|
+
inputSchema: {
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
subscription_id: { type: "string", description: "Local subscription ID" },
|
|
551
|
+
provider: { type: "string", description: "Provider ID to also enable the remote trigger (optional)" },
|
|
552
|
+
project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
|
|
553
|
+
},
|
|
554
|
+
required: ["subscription_id"],
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "disable_subscription",
|
|
559
|
+
description: "Disable a subscription so it stops routing events to the agent. Optionally also disables the remote trigger on the provider.",
|
|
560
|
+
inputSchema: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
subscription_id: { type: "string", description: "Local subscription ID" },
|
|
564
|
+
provider: { type: "string", description: "Provider ID to also disable the remote trigger (optional)" },
|
|
565
|
+
project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
|
|
566
|
+
},
|
|
567
|
+
required: ["subscription_id"],
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "delete_subscription",
|
|
572
|
+
description: "Delete a local subscription. Optionally also deletes the remote trigger on the provider.",
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: "object",
|
|
575
|
+
properties: {
|
|
576
|
+
subscription_id: { type: "string", description: "Local subscription ID" },
|
|
577
|
+
delete_remote: { type: "boolean", description: "Also delete the remote trigger on the provider (default false)" },
|
|
578
|
+
provider: { type: "string", description: "Provider ID (required if delete_remote is true)" },
|
|
579
|
+
project_id: { type: "string", description: "Project ID for API key resolution (optional)" },
|
|
580
|
+
},
|
|
581
|
+
required: ["subscription_id"],
|
|
582
|
+
},
|
|
583
|
+
},
|
|
450
584
|
];
|
|
451
585
|
|
|
452
586
|
// Tool execution handlers
|
|
@@ -964,6 +1098,221 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
|
|
|
964
1098
|
return { content: [{ type: "text", text: `Test "${tc.name}" deleted.` }] };
|
|
965
1099
|
}
|
|
966
1100
|
|
|
1101
|
+
// Subscription & Trigger tools
|
|
1102
|
+
case "list_trigger_providers": {
|
|
1103
|
+
const providerIds = getTriggerProviderIds();
|
|
1104
|
+
const projectId = args.project_id || null;
|
|
1105
|
+
const result = providerIds.map(id => {
|
|
1106
|
+
const provider = getTriggerProvider(id);
|
|
1107
|
+
const hasKey = !!ProviderKeys.getDecryptedForProject(id, projectId);
|
|
1108
|
+
return {
|
|
1109
|
+
id,
|
|
1110
|
+
name: provider?.name || id,
|
|
1111
|
+
hasKey,
|
|
1112
|
+
};
|
|
1113
|
+
});
|
|
1114
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
case "list_trigger_types": {
|
|
1118
|
+
const providerId = args.provider;
|
|
1119
|
+
const projectId = args.project_id || null;
|
|
1120
|
+
const triggerProvider = getTriggerProvider(providerId);
|
|
1121
|
+
if (!triggerProvider) {
|
|
1122
|
+
return { content: [{ type: "text", text: `Unknown trigger provider: ${providerId}. Available: ${getTriggerProviderIds().join(", ")}` }], isError: true };
|
|
1123
|
+
}
|
|
1124
|
+
const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
|
|
1125
|
+
if (!apiKey) {
|
|
1126
|
+
return { content: [{ type: "text", text: `${triggerProvider.name} API key not configured` }], isError: true };
|
|
1127
|
+
}
|
|
1128
|
+
const types = await triggerProvider.listTriggerTypes(apiKey);
|
|
1129
|
+
const result = types.map(t => ({
|
|
1130
|
+
slug: t.slug,
|
|
1131
|
+
name: t.name,
|
|
1132
|
+
description: t.description,
|
|
1133
|
+
type: t.type,
|
|
1134
|
+
toolkit_slug: t.toolkit_slug,
|
|
1135
|
+
toolkit_name: t.toolkit_name,
|
|
1136
|
+
config_schema: t.config_schema,
|
|
1137
|
+
}));
|
|
1138
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
case "list_connected_accounts": {
|
|
1142
|
+
const providerId = args.provider;
|
|
1143
|
+
const projectId = args.project_id || null;
|
|
1144
|
+
const integrationProvider = getProvider(providerId);
|
|
1145
|
+
if (!integrationProvider) {
|
|
1146
|
+
return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}` }], isError: true };
|
|
1147
|
+
}
|
|
1148
|
+
const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
|
|
1149
|
+
if (!apiKey) {
|
|
1150
|
+
return { content: [{ type: "text", text: `${integrationProvider.name} API key not configured` }], isError: true };
|
|
1151
|
+
}
|
|
1152
|
+
const accounts = await integrationProvider.listConnectedAccounts(apiKey, "platform-agent");
|
|
1153
|
+
const result = accounts.map(a => ({
|
|
1154
|
+
id: a.id,
|
|
1155
|
+
appName: a.appName,
|
|
1156
|
+
status: a.status,
|
|
1157
|
+
createdAt: a.createdAt,
|
|
1158
|
+
}));
|
|
1159
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
case "list_subscriptions": {
|
|
1163
|
+
let subscriptions;
|
|
1164
|
+
if (args.agent_id) {
|
|
1165
|
+
subscriptions = SubscriptionDB.findByAgentId(args.agent_id);
|
|
1166
|
+
} else {
|
|
1167
|
+
subscriptions = SubscriptionDB.findAll(args.project_id || null);
|
|
1168
|
+
}
|
|
1169
|
+
const result = subscriptions.map(s => {
|
|
1170
|
+
const agent = AgentDB.findById(s.agent_id);
|
|
1171
|
+
return {
|
|
1172
|
+
id: s.id,
|
|
1173
|
+
trigger_slug: s.trigger_slug,
|
|
1174
|
+
trigger_instance_id: s.trigger_instance_id,
|
|
1175
|
+
agent_id: s.agent_id,
|
|
1176
|
+
agent_name: agent?.name || "Unknown",
|
|
1177
|
+
enabled: s.enabled,
|
|
1178
|
+
project_id: s.project_id,
|
|
1179
|
+
created_at: s.created_at,
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
case "create_subscription": {
|
|
1186
|
+
const providerId = args.provider;
|
|
1187
|
+
const projectId = args.project_id || null;
|
|
1188
|
+
const triggerProvider = getTriggerProvider(providerId);
|
|
1189
|
+
if (!triggerProvider) {
|
|
1190
|
+
return { content: [{ type: "text", text: `Unknown trigger provider: ${providerId}. Available: ${getTriggerProviderIds().join(", ")}` }], isError: true };
|
|
1191
|
+
}
|
|
1192
|
+
const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
|
|
1193
|
+
if (!apiKey) {
|
|
1194
|
+
return { content: [{ type: "text", text: `${triggerProvider.name} API key not configured` }], isError: true };
|
|
1195
|
+
}
|
|
1196
|
+
// Validate agent exists
|
|
1197
|
+
const agent = AgentDB.findById(args.agent_id);
|
|
1198
|
+
if (!agent) {
|
|
1199
|
+
return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Auto-setup webhook if not already configured for this provider
|
|
1203
|
+
const existingWebhook = SettingsDB.get(`${providerId}_webhook_url`);
|
|
1204
|
+
if (!existingWebhook) {
|
|
1205
|
+
try {
|
|
1206
|
+
const instanceUrl = SettingsDB.get("instance_url");
|
|
1207
|
+
if (instanceUrl) {
|
|
1208
|
+
const webhookUrl = `${instanceUrl}/api/webhooks/${providerId}`;
|
|
1209
|
+
const webhookResult = await triggerProvider.setupWebhook(apiKey, webhookUrl);
|
|
1210
|
+
if (webhookResult.secret) {
|
|
1211
|
+
SettingsDB.set(`${providerId}_webhook_secret`, webhookResult.secret);
|
|
1212
|
+
}
|
|
1213
|
+
SettingsDB.set(`${providerId}_webhook_url`, webhookUrl);
|
|
1214
|
+
console.log(`[platform-mcp] Auto-configured ${providerId} webhook: ${webhookUrl}`);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (e) {
|
|
1217
|
+
console.warn(`[platform-mcp] Failed to auto-setup ${providerId} webhook:`, e);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Create remote trigger on the provider
|
|
1222
|
+
const config: Record<string, unknown> = {
|
|
1223
|
+
agent_id: args.agent_id,
|
|
1224
|
+
...(args.config || {}),
|
|
1225
|
+
};
|
|
1226
|
+
const triggerResult = await triggerProvider.createTrigger(apiKey, args.trigger_slug, args.connected_account_id, config);
|
|
1227
|
+
|
|
1228
|
+
// Create local subscription for webhook routing
|
|
1229
|
+
const subscription = SubscriptionDB.create({
|
|
1230
|
+
trigger_slug: args.trigger_slug,
|
|
1231
|
+
trigger_instance_id: triggerResult.triggerId || null,
|
|
1232
|
+
agent_id: args.agent_id,
|
|
1233
|
+
enabled: true,
|
|
1234
|
+
project_id: projectId,
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
console.log(`[platform-mcp] Created subscription: ${args.trigger_slug} (instance=${triggerResult.triggerId}) → agent ${agent.name} (${agent.id})`);
|
|
1238
|
+
return { content: [{ type: "text", text: `Subscription created:\n${JSON.stringify({
|
|
1239
|
+
subscription_id: subscription.id,
|
|
1240
|
+
trigger_slug: args.trigger_slug,
|
|
1241
|
+
trigger_instance_id: triggerResult.triggerId,
|
|
1242
|
+
agent: agent.name,
|
|
1243
|
+
provider: providerId,
|
|
1244
|
+
enabled: true,
|
|
1245
|
+
}, null, 2)}` }] };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
case "enable_subscription": {
|
|
1249
|
+
const sub = SubscriptionDB.findById(args.subscription_id);
|
|
1250
|
+
if (!sub) {
|
|
1251
|
+
return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
|
|
1252
|
+
}
|
|
1253
|
+
SubscriptionDB.update(args.subscription_id, { enabled: true });
|
|
1254
|
+
|
|
1255
|
+
// Also enable remote trigger if provider specified
|
|
1256
|
+
if (args.provider && sub.trigger_instance_id) {
|
|
1257
|
+
const triggerProvider = getTriggerProvider(args.provider);
|
|
1258
|
+
const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
|
|
1259
|
+
if (triggerProvider && apiKey) {
|
|
1260
|
+
try {
|
|
1261
|
+
await triggerProvider.enableTrigger(apiKey, sub.trigger_instance_id);
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
console.warn(`[platform-mcp] Failed to enable remote trigger ${sub.trigger_instance_id}:`, e);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" enabled` }] };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
case "disable_subscription": {
|
|
1271
|
+
const sub = SubscriptionDB.findById(args.subscription_id);
|
|
1272
|
+
if (!sub) {
|
|
1273
|
+
return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
|
|
1274
|
+
}
|
|
1275
|
+
SubscriptionDB.update(args.subscription_id, { enabled: false });
|
|
1276
|
+
|
|
1277
|
+
// Also disable remote trigger if provider specified
|
|
1278
|
+
if (args.provider && sub.trigger_instance_id) {
|
|
1279
|
+
const triggerProvider = getTriggerProvider(args.provider);
|
|
1280
|
+
const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
|
|
1281
|
+
if (triggerProvider && apiKey) {
|
|
1282
|
+
try {
|
|
1283
|
+
await triggerProvider.disableTrigger(apiKey, sub.trigger_instance_id);
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
console.warn(`[platform-mcp] Failed to disable remote trigger ${sub.trigger_instance_id}:`, e);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" disabled` }] };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
case "delete_subscription": {
|
|
1293
|
+
const sub = SubscriptionDB.findById(args.subscription_id);
|
|
1294
|
+
if (!sub) {
|
|
1295
|
+
return { content: [{ type: "text", text: `Subscription not found: ${args.subscription_id}` }], isError: true };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Delete remote trigger if requested
|
|
1299
|
+
if (args.delete_remote && args.provider && sub.trigger_instance_id) {
|
|
1300
|
+
const triggerProvider = getTriggerProvider(args.provider);
|
|
1301
|
+
const apiKey = triggerProvider ? ProviderKeys.getDecryptedForProject(args.provider, args.project_id || null) : null;
|
|
1302
|
+
if (triggerProvider && apiKey) {
|
|
1303
|
+
try {
|
|
1304
|
+
await triggerProvider.deleteTrigger(apiKey, sub.trigger_instance_id);
|
|
1305
|
+
console.log(`[platform-mcp] Deleted remote trigger ${sub.trigger_instance_id} on ${args.provider}`);
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
console.warn(`[platform-mcp] Failed to delete remote trigger ${sub.trigger_instance_id}:`, e);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
SubscriptionDB.delete(args.subscription_id);
|
|
1313
|
+
return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" deleted` }] };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
967
1316
|
default:
|
|
968
1317
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
969
1318
|
}
|
|
@@ -1020,8 +1369,10 @@ You can manage:
|
|
|
1020
1369
|
- SKILLS: Reusable instruction sets that specialize agent behavior. Use create_skill to create new skills (pass project_id from context to scope to the current project), then assign them to agents. Use list_skills, get_skill, create_skill, toggle_skill, assign_skill_to_agent, unassign_skill_from_agent, delete_skill.
|
|
1021
1370
|
- PROVIDERS: View which LLM providers have API keys configured.
|
|
1022
1371
|
- TESTS: Create and run automated tests for agent workflows. Tests send a message to an agent, then an LLM judge evaluates the response against success criteria. Use list_tests, create_test, run_test, run_all_tests, get_test_results, delete_test.
|
|
1372
|
+
- SUBSCRIPTIONS & TRIGGERS: Subscribe agents to external events (webhooks). Supports multiple providers (composio, agentdojo). Use list_trigger_providers → list_trigger_types → list_connected_accounts → create_subscription. Manage with enable_subscription, disable_subscription, delete_subscription, list_subscriptions.
|
|
1023
1373
|
|
|
1024
1374
|
Typical workflow: list_providers → create_agent → assign MCP servers/skills → start_agent.
|
|
1375
|
+
Subscription workflow: list_trigger_providers → list_trigger_types (pick trigger) → list_connected_accounts (pick account) → create_subscription (link trigger to agent).
|
|
1025
1376
|
Test workflow: create_test (set agent, message, eval criteria) → run_test → check results.
|
|
1026
1377
|
Always use list_providers first to check which providers have API keys before creating agents.`,
|
|
1027
1378
|
};
|
package/src/routes/api/agents.ts
CHANGED
|
@@ -27,9 +27,22 @@ export async function handleAgentRoutes(
|
|
|
27
27
|
): Promise<Response | null> {
|
|
28
28
|
// ==================== AGENT CRUD ====================
|
|
29
29
|
|
|
30
|
-
// GET /api/agents - List
|
|
30
|
+
// GET /api/agents - List agents (excludes meta agent), optionally filtered by project
|
|
31
31
|
if (path === "/api/agents" && method === "GET") {
|
|
32
|
-
const
|
|
32
|
+
const url = new URL(req.url);
|
|
33
|
+
const projectId = url.searchParams.get("project_id");
|
|
34
|
+
|
|
35
|
+
let agents;
|
|
36
|
+
if (projectId === "unassigned") {
|
|
37
|
+
// Agents with no project
|
|
38
|
+
agents = AgentDB.findByProject(null);
|
|
39
|
+
} else if (projectId) {
|
|
40
|
+
agents = AgentDB.findByProject(projectId);
|
|
41
|
+
} else {
|
|
42
|
+
agents = AgentDB.findAll();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
agents = agents.filter(a => a.id !== META_AGENT_ID);
|
|
33
46
|
return json({ agents: toApiAgentsBatch(agents) });
|
|
34
47
|
}
|
|
35
48
|
|
package/src/routes/api/system.ts
CHANGED
|
@@ -187,7 +187,18 @@ export async function handleSystemRoutes(
|
|
|
187
187
|
|
|
188
188
|
// GET /api/dashboard - Get dashboard statistics
|
|
189
189
|
if (path === "/api/dashboard" && method === "GET") {
|
|
190
|
-
const
|
|
190
|
+
const url = new URL(req.url);
|
|
191
|
+
const projectId = url.searchParams.get("project_id");
|
|
192
|
+
|
|
193
|
+
let agents = AgentDB.findAll();
|
|
194
|
+
|
|
195
|
+
// Filter agents by project if specified
|
|
196
|
+
if (projectId === "unassigned") {
|
|
197
|
+
agents = agents.filter(a => !a.project_id);
|
|
198
|
+
} else if (projectId) {
|
|
199
|
+
agents = agents.filter(a => a.project_id === projectId);
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
const runningAgents = agents.filter(a => a.status === "running" && a.port);
|
|
192
203
|
|
|
193
204
|
let totalTasks = 0;
|
package/src/routes/auth.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validatePassword,
|
|
10
10
|
REFRESH_TOKEN_EXPIRY,
|
|
11
11
|
} from "../auth";
|
|
12
|
+
import { Onboarding } from "../providers";
|
|
12
13
|
import {
|
|
13
14
|
getTokenFromRequest,
|
|
14
15
|
getRefreshTokenFromCookie,
|
|
@@ -26,11 +27,12 @@ function json(data: unknown, status = 200, headers: Record<string, string> = {})
|
|
|
26
27
|
export async function handleAuthRequest(req: Request, path: string): Promise<Response> {
|
|
27
28
|
const method = req.method;
|
|
28
29
|
|
|
29
|
-
// GET /api/auth/check - Check authentication status
|
|
30
|
+
// GET /api/auth/check - Check authentication status (includes onboarding to avoid extra round trip)
|
|
30
31
|
if (path === "/api/auth/check" && method === "GET") {
|
|
31
32
|
const token = getTokenFromRequest(req);
|
|
32
33
|
const status = getAuthStatus(token || undefined);
|
|
33
|
-
|
|
34
|
+
const onboarding = Onboarding.getStatus();
|
|
35
|
+
return json({ ...status, onboarding });
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// POST /api/auth/login - Login with username and password
|
|
@@ -108,10 +110,17 @@ export async function handleAuthRequest(req: Request, path: string): Promise<Res
|
|
|
108
110
|
// Set new refresh token cookie
|
|
109
111
|
const cookieHeader = createRefreshTokenCookie(result.refreshToken, REFRESH_TOKEN_EXPIRY);
|
|
110
112
|
|
|
113
|
+
// Include user info + onboarding to avoid extra /api/auth/me round trip
|
|
114
|
+
const payload = verifyAccessToken(result.accessToken);
|
|
115
|
+
const user = payload ? UserDB.findById(payload.userId) : null;
|
|
116
|
+
const onboarding = Onboarding.getStatus();
|
|
117
|
+
|
|
111
118
|
return json(
|
|
112
119
|
{
|
|
113
120
|
accessToken: result.accessToken,
|
|
114
121
|
expiresIn: result.expiresIn,
|
|
122
|
+
user: user ? { id: user.id, username: user.username, role: user.role } : undefined,
|
|
123
|
+
onboarding,
|
|
115
124
|
},
|
|
116
125
|
200,
|
|
117
126
|
{ "Set-Cookie": cookieHeader }
|
package/src/web/App.tsx
CHANGED
|
@@ -45,10 +45,11 @@ export function Dashboard({
|
|
|
45
45
|
|
|
46
46
|
const fetchDashboardData = useCallback(async () => {
|
|
47
47
|
try {
|
|
48
|
+
const projectParam = currentProjectId ? `project_id=${encodeURIComponent(currentProjectId)}` : "";
|
|
48
49
|
const [dashRes, tasksRes, activityRes] = await Promise.all([
|
|
49
|
-
authFetch(
|
|
50
|
-
authFetch(
|
|
51
|
-
authFetch(
|
|
50
|
+
authFetch(`/api/dashboard${projectParam ? `?${projectParam}` : ""}`),
|
|
51
|
+
authFetch(`/api/tasks?status=all${projectParam ? `&${projectParam}` : ""}`),
|
|
52
|
+
authFetch(`/api/telemetry/events?type=thread_activity&limit=20${projectParam ? `&${projectParam}` : ""}`),
|
|
52
53
|
]);
|
|
53
54
|
|
|
54
55
|
if (dashRes.ok) {
|
|
@@ -68,7 +69,7 @@ export function Dashboard({
|
|
|
68
69
|
} catch (e) {
|
|
69
70
|
console.error("Failed to fetch dashboard data:", e);
|
|
70
71
|
}
|
|
71
|
-
}, [authFetch]);
|
|
72
|
+
}, [authFetch, currentProjectId]);
|
|
72
73
|
|
|
73
74
|
useEffect(() => {
|
|
74
75
|
fetchDashboardData();
|
|
@@ -20,6 +20,8 @@ interface AuthContextValue {
|
|
|
20
20
|
hasUsers: boolean | null;
|
|
21
21
|
isDev: boolean;
|
|
22
22
|
accessToken: string | null;
|
|
23
|
+
onboardingComplete: boolean | null;
|
|
24
|
+
setOnboardingComplete: (v: boolean) => void;
|
|
23
25
|
login: (username: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
|
24
26
|
logout: () => Promise<void>;
|
|
25
27
|
refreshToken: () => Promise<boolean>;
|
|
@@ -47,6 +49,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
47
49
|
const [isLoading, setIsLoading] = useState(true);
|
|
48
50
|
const [hasUsers, setHasUsers] = useState<boolean | null>(null);
|
|
49
51
|
const [isDev, setIsDev] = useState(false);
|
|
52
|
+
const [onboardingComplete, setOnboardingComplete] = useState<boolean | null>(null);
|
|
50
53
|
|
|
51
54
|
// Refs to track state without causing re-renders
|
|
52
55
|
const tokenRef = useRef<string | null>(null);
|
|
@@ -80,18 +83,15 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
80
83
|
const data = await res.json();
|
|
81
84
|
updateToken(data.accessToken);
|
|
82
85
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const meData = await meRes.json();
|
|
90
|
-
setUser(meData.user);
|
|
91
|
-
return true;
|
|
86
|
+
// User info + onboarding included in refresh response to avoid extra round trip
|
|
87
|
+
if (data.user) {
|
|
88
|
+
setUser(data.user);
|
|
89
|
+
}
|
|
90
|
+
if (data.onboarding) {
|
|
91
|
+
setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
return
|
|
94
|
+
return !!data.user;
|
|
95
95
|
} catch (e) {
|
|
96
96
|
console.error("Token refresh failed:", e);
|
|
97
97
|
return false;
|
|
@@ -107,11 +107,16 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
107
107
|
const res = await fetch("/api/auth/check", {
|
|
108
108
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
109
109
|
});
|
|
110
|
-
const data: AuthStatus = await res.json();
|
|
110
|
+
const data: AuthStatus & { onboarding?: { completed: boolean; has_any_keys: boolean } } = await res.json();
|
|
111
111
|
|
|
112
112
|
setHasUsers(data.hasUsers);
|
|
113
113
|
setIsDev(data.isDev ?? false);
|
|
114
114
|
|
|
115
|
+
// Extract onboarding status (piggybacks on auth check to avoid extra round trip)
|
|
116
|
+
if (data.onboarding) {
|
|
117
|
+
setOnboardingComplete(data.onboarding.completed || data.onboarding.has_any_keys);
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
if (data.authenticated && data.user) {
|
|
116
121
|
setUser(data.user as User);
|
|
117
122
|
} else {
|
|
@@ -218,6 +223,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|
|
218
223
|
hasUsers,
|
|
219
224
|
isDev,
|
|
220
225
|
accessToken,
|
|
226
|
+
onboardingComplete,
|
|
227
|
+
setOnboardingComplete,
|
|
221
228
|
login,
|
|
222
229
|
logout,
|
|
223
230
|
refreshToken,
|
|
@@ -3,7 +3,7 @@ import type { Agent, AgentFeatures } from "../types";
|
|
|
3
3
|
import { useAuth } from "../context";
|
|
4
4
|
import { useAgentStatusChange } from "../context/TelemetryContext";
|
|
5
5
|
|
|
6
|
-
export function useAgents(enabled: boolean) {
|
|
6
|
+
export function useAgents(enabled: boolean, projectId?: string | null) {
|
|
7
7
|
const { accessToken } = useAuth();
|
|
8
8
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
9
9
|
const [loading, setLoading] = useState(true);
|
|
@@ -17,11 +17,15 @@ export function useAgents(enabled: boolean) {
|
|
|
17
17
|
}, [accessToken]);
|
|
18
18
|
|
|
19
19
|
const fetchAgents = useCallback(async () => {
|
|
20
|
-
|
|
20
|
+
let url = "/api/agents";
|
|
21
|
+
if (projectId !== undefined && projectId !== null) {
|
|
22
|
+
url += `?project_id=${encodeURIComponent(projectId)}`;
|
|
23
|
+
}
|
|
24
|
+
const res = await fetch(url, { headers: getHeaders() });
|
|
21
25
|
const data = await res.json();
|
|
22
26
|
setAgents(data.agents || []);
|
|
23
27
|
setLoading(false);
|
|
24
|
-
}, [getHeaders]);
|
|
28
|
+
}, [getHeaders, projectId]);
|
|
25
29
|
|
|
26
30
|
// Fetch on mount + auto-refetch when agents start/stop/crash (via SSE telemetry)
|
|
27
31
|
const statusChangeCounter = useAgentStatusChange();
|
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import { useAuth } from "../context";
|
|
3
3
|
|
|
4
4
|
export function useOnboarding() {
|
|
5
|
-
const {
|
|
6
|
-
const [isComplete, setIsComplete] = useState<boolean | null>(null);
|
|
5
|
+
const { authFetch, onboardingComplete, setOnboardingComplete } = useAuth();
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (accessToken) {
|
|
11
|
-
headers.Authorization = `Bearer ${accessToken}`;
|
|
12
|
-
}
|
|
13
|
-
return headers;
|
|
14
|
-
}, [accessToken]);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
// Only check onboarding status when authenticated
|
|
18
|
-
if (!isAuthenticated) {
|
|
19
|
-
setIsComplete(null);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
fetch("/api/onboarding/status", { headers: getHeaders() })
|
|
24
|
-
.then(res => res.json())
|
|
25
|
-
.then(data => {
|
|
26
|
-
setIsComplete(data.completed || data.has_any_keys);
|
|
27
|
-
})
|
|
28
|
-
.catch(() => setIsComplete(true)); // Fallback to showing app
|
|
29
|
-
}, [isAuthenticated, getHeaders]);
|
|
7
|
+
// Onboarding status is now included in the /api/auth/check response,
|
|
8
|
+
// so no separate fetch is needed. This eliminates one round trip on load.
|
|
30
9
|
|
|
31
10
|
const complete = useCallback(async () => {
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
}, [
|
|
11
|
+
await authFetch("/api/onboarding/complete", { method: "POST" });
|
|
12
|
+
setOnboardingComplete(true);
|
|
13
|
+
}, [authFetch, setOnboardingComplete]);
|
|
35
14
|
|
|
36
15
|
return {
|
|
37
|
-
isComplete,
|
|
38
|
-
setIsComplete,
|
|
16
|
+
isComplete: onboardingComplete,
|
|
17
|
+
setIsComplete: setOnboardingComplete,
|
|
39
18
|
complete,
|
|
40
19
|
};
|
|
41
20
|
}
|