apteva 0.4.18 → 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.kf6bbwkk.js +4 -0
- package/dist/{App.nps62kvt.js → App.039re6cf.js} +3 -3
- package/dist/App.2jmkqm8c.js +4 -0
- package/dist/{App.np463xvy.js → App.2yy66bnp.js} +3 -3
- package/dist/App.3515wsb4.js +4 -0
- package/dist/App.7v1w3ys9.js +4 -0
- package/dist/{App.nft7h9jt.js → App.c90t3dxg.js} +3 -3
- package/dist/App.edwahsvz.js +4 -0
- package/dist/App.jfx3der4.js +4 -0
- package/dist/App.n4jb3c22.js +13 -0
- package/dist/{App.mq6jqare.js → App.p02f4ret.js} +1 -1
- package/dist/App.q3bpx15d.js +20 -0
- package/dist/App.r0a2nmqs.js +267 -0
- package/dist/App.s2yrcz15.js +4 -0
- package/dist/App.s5j82a5j.js +4 -0
- package/dist/App.tg1b94tx.js +4 -0
- 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/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -4
- package/src/channels/index.ts +40 -0
- package/src/channels/telegram.ts +306 -0
- package/src/db.ts +180 -0
- package/src/integrations/agentdojo.ts +1 -1
- package/src/mcp-handler.ts +31 -24
- package/src/mcp-platform.ts +353 -2
- package/src/providers.ts +22 -9
- package/src/routes/api/agents.ts +15 -2
- package/src/routes/api/channels.ts +182 -0
- package/src/routes/api/integrations.ts +13 -5
- package/src/routes/api/mcp.ts +27 -9
- package/src/routes/api/system.ts +12 -1
- package/src/routes/api/telemetry.ts +30 -0
- package/src/routes/api/triggers.ts +22 -2
- package/src/routes/api.ts +3 -1
- package/src/routes/auth.ts +11 -2
- package/src/server.ts +39 -4
- package/src/triggers/agentdojo.ts +23 -18
- package/src/tui/AgentList.tsx +145 -0
- package/src/tui/App.tsx +102 -0
- package/src/tui/Login.tsx +104 -0
- package/src/tui/api.ts +72 -0
- package/src/tui/index.tsx +7 -0
- package/src/web/App.tsx +2 -2
- package/src/web/components/agents/AgentPanel.tsx +4 -37
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/connections/OverviewTab.tsx +22 -68
- package/src/web/components/connections/TriggersTab.tsx +549 -70
- package/src/web/components/dashboard/Dashboard.tsx +5 -4
- package/src/web/components/layout/Header.tsx +196 -4
- package/src/web/components/settings/SettingsPage.tsx +269 -1
- package/src/web/context/AuthContext.tsx +18 -11
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/context/index.ts +1 -1
- package/src/web/hooks/useAgents.ts +7 -3
- package/src/web/hooks/useOnboarding.ts +9 -30
- package/dist/ActivityPage.yv28a2vj.js +0 -3
- package/dist/ApiDocsPage.4ccwjjbk.js +0 -4
- package/dist/App.155wke5v.js +0 -4
- package/dist/App.2e19nvn4.js +0 -13
- package/dist/App.2ye1b5n0.js +0 -4
- package/dist/App.4da4ycbe.js +0 -4
- package/dist/App.b6wtzd1j.js +0 -4
- package/dist/App.fjrh28tf.js +0 -4
- package/dist/App.htc36cy8.js +0 -4
- package/dist/App.me6reaa6.js +0 -4
- package/dist/App.n5q6p960.js +0 -4
- package/dist/App.q8ws33cc.js +0 -181
- package/dist/App.tb0y0jmt.js +0 -40
- package/dist/ConnectionsPage.52evzrp7.js +0 -3
- package/dist/McpPage.bjqrp0n2.js +0 -3
- package/dist/SettingsPage.es76hnj2.js +0 -3
- package/dist/SkillsPage.06h8yf0h.js +0 -3
- package/dist/TasksPage.99df66mk.js +0 -3
- package/dist/TelemetryPage.bmdnxhq7.js +0 -3
- package/dist/TestsPage.denxrg8c.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/providers.ts
CHANGED
|
@@ -198,35 +198,48 @@ export const ProviderKeys = {
|
|
|
198
198
|
}
|
|
199
199
|
},
|
|
200
200
|
|
|
201
|
-
// Get decrypted API key for a provider (global key)
|
|
201
|
+
// Get decrypted API key for a provider (global key, falls back to env var)
|
|
202
202
|
getDecrypted(providerId: string): string | null {
|
|
203
203
|
const record = ProviderKeysDB.findByProvider(providerId);
|
|
204
|
-
if (
|
|
204
|
+
if (record) {
|
|
205
|
+
try {
|
|
206
|
+
return decrypt(record.encrypted_key);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error(`Failed to decrypt key for ${providerId}:`, err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
205
211
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
212
|
+
// Fall back to environment variable
|
|
213
|
+
const provider = PROVIDERS[providerId as ProviderId];
|
|
214
|
+
if (provider?.envVar) {
|
|
215
|
+
const envVal = process.env[provider.envVar];
|
|
216
|
+
if (envVal) return envVal;
|
|
211
217
|
}
|
|
218
|
+
return null;
|
|
212
219
|
},
|
|
213
220
|
|
|
214
221
|
// Get decrypted API key for a provider and project
|
|
215
222
|
// Falls back to global key if no project-specific key exists
|
|
216
223
|
getDecryptedForProject(providerId: string, projectId: string | null): string | null {
|
|
224
|
+
console.log(`[ProviderKeys.getDecryptedForProject] providerId=${providerId}, projectId=${projectId}`);
|
|
217
225
|
// Try project-specific key first
|
|
218
226
|
if (projectId) {
|
|
219
227
|
const projectRecord = ProviderKeysDB.findByProviderAndProject(providerId, projectId);
|
|
228
|
+
console.log(`[ProviderKeys.getDecryptedForProject] project record found: ${!!projectRecord}`);
|
|
220
229
|
if (projectRecord) {
|
|
221
230
|
try {
|
|
222
|
-
|
|
231
|
+
const key = decrypt(projectRecord.encrypted_key);
|
|
232
|
+
console.log(`[ProviderKeys.getDecryptedForProject] decrypted project key OK, length=${key?.length}`);
|
|
233
|
+
return key;
|
|
223
234
|
} catch (err) {
|
|
224
235
|
console.error(`Failed to decrypt project key for ${providerId}/${projectId}:`, err);
|
|
225
236
|
}
|
|
226
237
|
}
|
|
227
238
|
}
|
|
228
239
|
// Fall back to global key
|
|
229
|
-
|
|
240
|
+
const globalKey = this.getDecrypted(providerId);
|
|
241
|
+
console.log(`[ProviderKeys.getDecryptedForProject] global fallback: found=${!!globalKey}, length=${globalKey?.length || 0}`);
|
|
242
|
+
return globalKey;
|
|
230
243
|
},
|
|
231
244
|
|
|
232
245
|
// Check if a provider has a key configured (global)
|
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
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { json } from "./helpers";
|
|
2
|
+
import { ChannelDB, AgentDB } from "../../db";
|
|
3
|
+
import { encryptObject } from "../../crypto";
|
|
4
|
+
import { startChannel, stopChannel } from "../../channels";
|
|
5
|
+
|
|
6
|
+
export async function handleChannelRoutes(
|
|
7
|
+
req: Request,
|
|
8
|
+
path: string,
|
|
9
|
+
method: string,
|
|
10
|
+
): Promise<Response | null> {
|
|
11
|
+
// GET /api/channels - List all channels
|
|
12
|
+
if (path === "/api/channels" && method === "GET") {
|
|
13
|
+
const channels = ChannelDB.findAll();
|
|
14
|
+
// Strip encrypted config from list response
|
|
15
|
+
const safe = channels.map(ch => ({
|
|
16
|
+
id: ch.id,
|
|
17
|
+
type: ch.type,
|
|
18
|
+
name: ch.name,
|
|
19
|
+
agent_id: ch.agent_id,
|
|
20
|
+
status: ch.status,
|
|
21
|
+
error: ch.error,
|
|
22
|
+
project_id: ch.project_id,
|
|
23
|
+
created_at: ch.created_at,
|
|
24
|
+
updated_at: ch.updated_at,
|
|
25
|
+
}));
|
|
26
|
+
return json({ channels: safe });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// POST /api/channels - Create a new channel
|
|
30
|
+
if (path === "/api/channels" && method === "POST") {
|
|
31
|
+
const body = await req.json();
|
|
32
|
+
const { type, name, agent_id, config, project_id } = body;
|
|
33
|
+
|
|
34
|
+
if (!type || !name || !agent_id || !config) {
|
|
35
|
+
return json({ error: "Missing required fields: type, name, agent_id, config" }, 400);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (type !== "telegram") {
|
|
39
|
+
return json({ error: `Unsupported channel type: ${type}. Supported: telegram` }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate agent exists
|
|
43
|
+
const agent = AgentDB.findById(agent_id);
|
|
44
|
+
if (!agent) {
|
|
45
|
+
return json({ error: "Agent not found" }, 404);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate config has required fields
|
|
49
|
+
if (!config.botToken) {
|
|
50
|
+
return json({ error: "Missing botToken in config" }, 400);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Encrypt config before storing
|
|
54
|
+
const encryptedConfig = encryptObject(config);
|
|
55
|
+
|
|
56
|
+
const channel = ChannelDB.create({
|
|
57
|
+
type,
|
|
58
|
+
name,
|
|
59
|
+
agent_id,
|
|
60
|
+
config: encryptedConfig,
|
|
61
|
+
project_id: project_id || null,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return json({
|
|
65
|
+
channel: {
|
|
66
|
+
id: channel.id,
|
|
67
|
+
type: channel.type,
|
|
68
|
+
name: channel.name,
|
|
69
|
+
agent_id: channel.agent_id,
|
|
70
|
+
status: channel.status,
|
|
71
|
+
error: channel.error,
|
|
72
|
+
project_id: channel.project_id,
|
|
73
|
+
created_at: channel.created_at,
|
|
74
|
+
},
|
|
75
|
+
}, 201);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Routes with channel ID
|
|
79
|
+
const channelMatch = path.match(/^\/api\/channels\/([^/]+)$/);
|
|
80
|
+
const channelActionMatch = path.match(/^\/api\/channels\/([^/]+)\/(start|stop)$/);
|
|
81
|
+
|
|
82
|
+
// GET /api/channels/:id - Get channel detail
|
|
83
|
+
if (channelMatch && method === "GET") {
|
|
84
|
+
const channel = ChannelDB.findById(channelMatch[1]);
|
|
85
|
+
if (!channel) return json({ error: "Channel not found" }, 404);
|
|
86
|
+
|
|
87
|
+
return json({
|
|
88
|
+
channel: {
|
|
89
|
+
id: channel.id,
|
|
90
|
+
type: channel.type,
|
|
91
|
+
name: channel.name,
|
|
92
|
+
agent_id: channel.agent_id,
|
|
93
|
+
status: channel.status,
|
|
94
|
+
error: channel.error,
|
|
95
|
+
project_id: channel.project_id,
|
|
96
|
+
created_at: channel.created_at,
|
|
97
|
+
updated_at: channel.updated_at,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// PUT /api/channels/:id - Update channel
|
|
103
|
+
if (channelMatch && method === "PUT") {
|
|
104
|
+
const channel = ChannelDB.findById(channelMatch[1]);
|
|
105
|
+
if (!channel) return json({ error: "Channel not found" }, 404);
|
|
106
|
+
|
|
107
|
+
if (channel.status === "running") {
|
|
108
|
+
return json({ error: "Stop the channel before updating" }, 400);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const body = await req.json();
|
|
112
|
+
const updates: Record<string, any> = {};
|
|
113
|
+
|
|
114
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
115
|
+
if (body.agent_id !== undefined) {
|
|
116
|
+
const agent = AgentDB.findById(body.agent_id);
|
|
117
|
+
if (!agent) return json({ error: "Agent not found" }, 404);
|
|
118
|
+
updates.agent_id = body.agent_id;
|
|
119
|
+
}
|
|
120
|
+
if (body.config !== undefined) {
|
|
121
|
+
if (!body.config.botToken) {
|
|
122
|
+
return json({ error: "Missing botToken in config" }, 400);
|
|
123
|
+
}
|
|
124
|
+
updates.config = encryptObject(body.config);
|
|
125
|
+
}
|
|
126
|
+
if (body.project_id !== undefined) updates.project_id = body.project_id;
|
|
127
|
+
|
|
128
|
+
const updated = ChannelDB.update(channelMatch[1], updates);
|
|
129
|
+
return json({
|
|
130
|
+
channel: updated ? {
|
|
131
|
+
id: updated.id,
|
|
132
|
+
type: updated.type,
|
|
133
|
+
name: updated.name,
|
|
134
|
+
agent_id: updated.agent_id,
|
|
135
|
+
status: updated.status,
|
|
136
|
+
project_id: updated.project_id,
|
|
137
|
+
} : null,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// DELETE /api/channels/:id - Delete channel
|
|
142
|
+
if (channelMatch && method === "DELETE") {
|
|
143
|
+
const channel = ChannelDB.findById(channelMatch[1]);
|
|
144
|
+
if (!channel) return json({ error: "Channel not found" }, 404);
|
|
145
|
+
|
|
146
|
+
// Stop if running
|
|
147
|
+
if (channel.status === "running") {
|
|
148
|
+
await stopChannel(channel.id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ChannelDB.delete(channelMatch[1]);
|
|
152
|
+
return json({ deleted: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// POST /api/channels/:id/start - Start channel
|
|
156
|
+
if (channelActionMatch && channelActionMatch[2] === "start" && method === "POST") {
|
|
157
|
+
const channel = ChannelDB.findById(channelActionMatch[1]);
|
|
158
|
+
if (!channel) return json({ error: "Channel not found" }, 404);
|
|
159
|
+
|
|
160
|
+
if (channel.status === "running") {
|
|
161
|
+
return json({ error: "Channel is already running" }, 400);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await startChannel(channel.id);
|
|
165
|
+
if (!result.success) {
|
|
166
|
+
return json({ error: result.error || "Failed to start channel" }, 500);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return json({ started: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// POST /api/channels/:id/stop - Stop channel
|
|
173
|
+
if (channelActionMatch && channelActionMatch[2] === "stop" && method === "POST") {
|
|
174
|
+
const channel = ChannelDB.findById(channelActionMatch[1]);
|
|
175
|
+
if (!channel) return json({ error: "Channel not found" }, 404);
|
|
176
|
+
|
|
177
|
+
await stopChannel(channel.id);
|
|
178
|
+
return json({ stopped: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|