apteva 0.4.32 → 0.4.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/ActivityPage.c48n83h2.js +3 -0
  2. package/dist/ApiDocsPage.yzcxx5ax.js +4 -0
  3. package/dist/App.09yb8t0b.js +1 -0
  4. package/dist/App.152mbs1r.js +4 -0
  5. package/dist/App.3a67nx9w.js +4 -0
  6. package/dist/App.9epx6785.js +4 -0
  7. package/dist/App.d8955awp.js +4 -0
  8. package/dist/App.drwb57jq.js +4 -0
  9. package/dist/App.gssbmajb.js +4 -0
  10. package/dist/App.qw70pc29.js +53 -0
  11. package/dist/App.qzbx5wtj.js +4 -0
  12. package/dist/App.r5serxkt.js +8 -0
  13. package/dist/App.tpmp9020.js +20 -0
  14. package/dist/App.v2wb4d7d.js +61 -0
  15. package/dist/App.vxmaaj0m.js +13 -0
  16. package/dist/App.w4p2tda9.js +4 -0
  17. package/dist/App.wv2ng55q.js +221 -0
  18. package/dist/App.yncnrn0f.js +4 -0
  19. package/dist/ConnectionsPage.k6cspyqq.js +3 -0
  20. package/dist/McpPage.cdxm48xj.js +3 -0
  21. package/dist/SettingsPage.evpv7c2y.js +3 -0
  22. package/dist/SkillsPage.pvzp6c1a.js +3 -0
  23. package/dist/TasksPage.6jnvbpsy.js +3 -0
  24. package/dist/TelemetryPage.t7vk24zc.js +3 -0
  25. package/dist/TestsPage.5x6658aa.js +3 -0
  26. package/dist/ThreadsPage.3fvhtevh.js +3 -0
  27. package/dist/apteva-kit.css +1 -1
  28. package/dist/index.html +1 -1
  29. package/dist/styles.css +1 -1
  30. package/package.json +10 -9
  31. package/src/crypto.ts +4 -3
  32. package/src/db.ts +171 -36
  33. package/src/integrations/agentdojo.ts +95 -12
  34. package/src/integrations/index.ts +7 -0
  35. package/src/mcp-platform.ts +870 -142
  36. package/src/openapi.ts +96 -0
  37. package/src/providers.ts +60 -34
  38. package/src/routes/api/agent-utils.ts +59 -47
  39. package/src/routes/api/agents.ts +71 -2
  40. package/src/routes/api/integrations.ts +11 -5
  41. package/src/routes/api/mcp.ts +5 -4
  42. package/src/routes/api/meta-agent.ts +37 -1
  43. package/src/routes/api/projects.ts +3 -3
  44. package/src/routes/api/providers.ts +121 -30
  45. package/src/routes/api/skills.ts +2 -3
  46. package/src/routes/api/system.ts +98 -14
  47. package/src/routes/api/telemetry.ts +19 -1
  48. package/src/routes/share.ts +85 -0
  49. package/src/server.ts +43 -32
  50. package/src/triggers/agentdojo.ts +2 -2
  51. package/src/web/App.tsx +107 -21
  52. package/src/web/components/activity/ActivityPage.tsx +242 -389
  53. package/src/web/components/agents/AgentCard.tsx +19 -27
  54. package/src/web/components/agents/AgentPanel.tsx +358 -198
  55. package/src/web/components/agents/AgentsView.tsx +4 -4
  56. package/src/web/components/agents/CreateAgentModal.tsx +21 -79
  57. package/src/web/components/api/ApiDocsPage.tsx +66 -66
  58. package/src/web/components/auth/CreateAccountStep.tsx +16 -16
  59. package/src/web/components/auth/LoginPage.tsx +10 -10
  60. package/src/web/components/common/Icons.tsx +8 -0
  61. package/src/web/components/common/LoadingSpinner.tsx +2 -2
  62. package/src/web/components/common/Modal.tsx +8 -8
  63. package/src/web/components/common/Select.tsx +11 -10
  64. package/src/web/components/connections/ConnectionsPage.tsx +4 -4
  65. package/src/web/components/connections/IntegrationsTab.tsx +18 -18
  66. package/src/web/components/connections/OverviewTab.tsx +13 -13
  67. package/src/web/components/connections/TriggersTab.tsx +99 -99
  68. package/src/web/components/dashboard/Dashboard.tsx +177 -52
  69. package/src/web/components/index.ts +1 -1
  70. package/src/web/components/layout/Header.tsx +50 -34
  71. package/src/web/components/layout/Sidebar.tsx +41 -16
  72. package/src/web/components/mcp/IntegrationsPanel.tsx +160 -69
  73. package/src/web/components/mcp/McpPage.tsx +218 -209
  74. package/src/web/components/meta-agent/MetaAgent.tsx +15 -11
  75. package/src/web/components/onboarding/OnboardingWizard.tsx +25 -25
  76. package/src/web/components/settings/SettingsPage.tsx +389 -221
  77. package/src/web/components/skills/SkillsPage.tsx +88 -88
  78. package/src/web/components/tasks/TasksPage.tsx +385 -68
  79. package/src/web/components/telemetry/TelemetryPage.tsx +294 -39
  80. package/src/web/components/tests/TestsPage.tsx +50 -50
  81. package/src/web/components/threads/ThreadsPage.tsx +315 -0
  82. package/src/web/context/AuthContext.tsx +3 -3
  83. package/src/web/context/ProjectContext.tsx +8 -3
  84. package/src/web/context/TelemetryContext.tsx +24 -6
  85. package/src/web/context/ThemeContext.tsx +69 -0
  86. package/src/web/context/index.ts +3 -1
  87. package/src/web/styles.css +25 -7
  88. package/src/web/themes.ts +99 -0
  89. package/src/web/types.ts +4 -7
  90. package/dist/ActivityPage.41nbye4r.js +0 -3
  91. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  92. package/dist/App.0sbax9et.js +0 -4
  93. package/dist/App.0ws427h8.js +0 -4
  94. package/dist/App.6q6bar8b.js +0 -4
  95. package/dist/App.80301vdb.js +0 -4
  96. package/dist/App.af2wg84v.js +0 -267
  97. package/dist/App.ca1rz1ph.js +0 -4
  98. package/dist/App.ensa6z0r.js +0 -4
  99. package/dist/App.f8g7tych.js +0 -13
  100. package/dist/App.mvtqv6qc.js +0 -20
  101. package/dist/App.ncgc9cxy.js +0 -4
  102. package/dist/App.p02f4ret.js +0 -1
  103. package/dist/App.p0fb1pds.js +0 -4
  104. package/dist/App.pmaq48sj.js +0 -4
  105. package/dist/App.yv87t9m5.js +0 -4
  106. package/dist/App.zjmfm8p6.js +0 -4
  107. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  108. package/dist/McpPage.y396h6fy.js +0 -3
  109. package/dist/SettingsPage.p1hc60gk.js +0 -3
  110. package/dist/SkillsPage.yj3xdsay.js +0 -3
  111. package/dist/TasksPage.sjv0khtv.js +0 -3
  112. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  113. package/dist/TestsPage.zzs4qfj8.js +0 -3
@@ -1,18 +1,30 @@
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, SubscriptionDB, SettingsDB, generateId } from "./db";
4
+ import { AgentDB, ProjectDB, McpServerDB, McpServerToolDB, SkillDB, TelemetryDB, SubscriptionDB, SettingsDB, generateId } from "./db";
5
5
  import { TestCaseDB, TestRunDB } from "./db-tests";
6
6
  import { runTest, runAll } from "./test-runner";
7
7
  import { getProvidersWithStatus, PROVIDERS, ProviderKeys } from "./providers";
8
- import { startAgentProcess, setAgentStatus, toApiAgent, META_AGENT_ID, agentFetch } from "./routes/api/agent-utils";
8
+ import { startAgentProcess, setAgentStatus, toApiAgent, META_AGENT_ID, agentFetch, fetchFromAgent } from "./routes/api/agent-utils";
9
9
  import { agentProcesses } from "./server";
10
10
  import { getTriggerProvider, getTriggerProviderIds, registerTriggerProvider } from "./triggers";
11
11
  import { ComposioTriggerProvider } from "./triggers/composio";
12
12
  import { AgentDojoTriggerProvider } from "./triggers/agentdojo";
13
- import { getProvider, registerProvider } from "./integrations";
13
+ import { getProvider, getProviderIds, registerProvider } from "./integrations";
14
14
  import { ComposioProvider } from "./integrations/composio";
15
- import { AgentDojoProvider } from "./integrations/agentdojo";
15
+ import {
16
+ AgentDojoProvider,
17
+ listServers as listAgentDojoServers,
18
+ createServer as createAgentDojoServer,
19
+ getServer as getAgentDojoServer,
20
+ } from "./integrations/agentdojo";
21
+ import {
22
+ listMcpServers as listComposioServers,
23
+ createMcpServer as createComposioServer,
24
+ getAuthConfigForToolkit as getComposioAuthConfig,
25
+ getUserIdForAuthConfig as getComposioUserForAuth,
26
+ createMcpServerInstance as createComposioInstance,
27
+ } from "./integrations/composio";
16
28
 
17
29
  // Register trigger + integration providers on module load
18
30
  registerTriggerProvider(ComposioTriggerProvider);
@@ -30,6 +42,7 @@ interface JsonRpcRequest {
30
42
  params?: any;
31
43
  }
32
44
 
45
+
33
46
  interface JsonRpcResponse {
34
47
  jsonrpc: "2.0";
35
48
  id: number;
@@ -65,7 +78,7 @@ const PLATFORM_TOOLS = [
65
78
  description: `Create a new AI agent. The provider must have an API key configured — use list_providers first to check.
66
79
 
67
80
  PROVIDERS & MODELS (use list_providers to see which have keys):
68
- - anthropic: claude-sonnet-4-5 (recommended), claude-haiku-4-5 (fast/cheap)
81
+ - anthropic: claude-sonnet-4-6 (recommended), claude-sonnet-4-5, claude-haiku-4-5 (fast/cheap)
69
82
  - openai: gpt-4o (recommended), gpt-4o-mini (fast/cheap)
70
83
  - groq: llama-3.3-70b-versatile (recommended), llama-3.1-8b-instant (fast)
71
84
  - gemini: gemini-3-pro-preview (recommended), gemini-3-flash-preview (fast)
@@ -111,7 +124,14 @@ TIPS:
111
124
  },
112
125
  {
113
126
  name: "update_agent",
114
- description: "Update an existing agent's configuration. Only provide fields you want to change. If the agent is running, restart it after updating for changes to take effect.",
127
+ description: `Update an existing agent's configuration. Only provide fields you want to change. If the agent is running, restart it after updating for changes to take effect.
128
+
129
+ SKILLS & MCP SERVERS:
130
+ - Pass skill_ids or mcp_server_ids to SET the full list (replaces existing).
131
+ - Use add_skills / remove_skills to add/remove individual skills without replacing the whole list.
132
+ - Use add_mcp_servers / remove_mcp_servers to add/remove individual MCP servers.
133
+ - Adding MCP servers automatically enables the mcp feature.
134
+ - Use list_skills and list_mcp_servers to find IDs.`,
115
135
  inputSchema: {
116
136
  type: "object",
117
137
  properties: {
@@ -132,6 +152,12 @@ TIPS:
132
152
  files: { type: "boolean" },
133
153
  },
134
154
  },
155
+ skill_ids: { type: "array", items: { type: "string" }, description: "Set the full list of skill IDs (replaces existing)" },
156
+ add_skills: { type: "array", items: { type: "string" }, description: "Skill IDs to add (keeps existing)" },
157
+ remove_skills: { type: "array", items: { type: "string" }, description: "Skill IDs to remove" },
158
+ mcp_server_ids: { type: "array", items: { type: "string" }, description: "Set the full list of MCP server IDs (replaces existing)" },
159
+ add_mcp_servers: { type: "array", items: { type: "string" }, description: "MCP server IDs to add (keeps existing)" },
160
+ remove_mcp_servers: { type: "array", items: { type: "string" }, description: "MCP server IDs to remove" },
135
161
  },
136
162
  required: ["agent_id"],
137
163
  },
@@ -190,6 +216,31 @@ TIPS:
190
216
  required: ["name"],
191
217
  },
192
218
  },
219
+ {
220
+ name: "update_project",
221
+ description: "Update an existing project's name, description, or color.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ project_id: { type: "string", description: "The project ID to update" },
226
+ name: { type: "string", description: "New project name" },
227
+ description: { type: "string", description: "New project description" },
228
+ color: { type: "string", description: "New hex color code (e.g. #6366f1)" },
229
+ },
230
+ required: ["project_id"],
231
+ },
232
+ },
233
+ {
234
+ name: "delete_project",
235
+ description: "Delete a project. Agents, MCP servers, and skills in the project will be unassigned (not deleted).",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ project_id: { type: "string", description: "The project ID to delete" },
240
+ },
241
+ required: ["project_id"],
242
+ },
243
+ },
193
244
  {
194
245
  name: "list_providers",
195
246
  description: "List all available LLM providers and their configuration status (which have API keys).",
@@ -224,12 +275,13 @@ TIPS:
224
275
  description: `Create a new MCP server configuration. MCP servers provide tools that agents can use (web search, file access, APIs, etc).
225
276
 
226
277
  SERVER TYPES:
278
+ - local: Runs inside the apteva server — no external process needed. Add tools with add_tool_to_local_server. Ready to use immediately after adding tools.
227
279
  - http: Remote MCP server accessible via URL. Provide url and optional auth headers. Ready to use immediately.
228
280
  - npm: Node.js MCP server from npm. Provide package name (e.g. '@modelcontextprotocol/server-filesystem'). Needs to be started.
229
281
  - pip: Python MCP server from PyPI. Provide package name. Needs to be started.
230
282
  - custom: Custom command. Provide command and args. Needs to be started.
231
283
 
232
- After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers work immediately; npm/pip/custom servers need to be started from the MCP page in the UI.`,
284
+ After creating, assign to agents with assign_mcp_server_to_agent. Local and HTTP servers work immediately; npm/pip/custom servers need to be started from the MCP page in the UI.`,
233
285
  inputSchema: {
234
286
  type: "object",
235
287
  properties: {
@@ -257,27 +309,56 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
257
309
  },
258
310
  },
259
311
  {
260
- name: "assign_mcp_server_to_agent",
261
- description: "Assign an MCP server to an agent so the agent can use its tools. This automatically enables the MCP feature on the agent. If the agent is running, restart it for changes to take effect.",
312
+ name: "add_tool_to_local_server",
313
+ description: `Add a tool to a local MCP server. Only works for servers with type "local".
314
+
315
+ HANDLER TYPES:
316
+ - mock: Returns a static/templated response. Use mock_response with template variables: {{args.field}}, {{uuid()}}, {{now}}, {{timestamp}}, {{random_int(min,max)}}.
317
+ - http: Makes a real HTTP API call. Provide http_config with method, url, headers, and optional body. Templates work in url/headers/body. Use {{credential.KEY}} to reference server env vars for auth.
318
+ - javascript: Runs custom JavaScript code. The code receives args, credentials, and helper functions (uuid, now, timestamp, random_int, random_float). Return a value or JSON object.
319
+
320
+ EXAMPLES:
321
+ - Mock: handler_type="mock", mock_response={"greeting": "Hello {{args.name}}!", "id": "{{uuid()}}"}
322
+ - HTTP: handler_type="http", http_config={"method": "GET", "url": "https://api.example.com/users/{{args.user_id}}", "headers": {"Authorization": "Bearer {{credential.API_KEY}}"}}
323
+ - JavaScript: handler_type="javascript", code="return { sum: args.a + args.b, computed_at: now }"`,
262
324
  inputSchema: {
263
325
  type: "object",
264
326
  properties: {
265
- agent_id: { type: "string", description: "The agent ID" },
266
- server_id: { type: "string", description: "The MCP server ID to assign" },
327
+ server_id: { type: "string", description: "The local MCP server ID" },
328
+ name: { type: "string", description: "Tool name (snake_case, e.g. 'get_weather', 'create_ticket')" },
329
+ description: { type: "string", description: "What this tool does (shown to the agent)" },
330
+ input_schema: {
331
+ type: "object",
332
+ description: "JSON Schema for tool parameters. Must have type='object' with properties.",
333
+ },
334
+ handler_type: { type: "string", description: "How the tool executes: mock, http, or javascript" },
335
+ mock_response: { type: "object", description: "For mock handler: the response template (supports {{args.field}}, {{uuid()}}, {{now}})" },
336
+ http_config: {
337
+ type: "object",
338
+ description: "For http handler: { method, url, headers?, body? }. Templates supported in all fields.",
339
+ },
340
+ code: { type: "string", description: "For javascript handler: JS code to execute. Has access to args, credentials, uuid, now, timestamp, random_int, random_float. Must return a value." },
267
341
  },
268
- required: ["agent_id", "server_id"],
342
+ required: ["server_id", "name", "description", "input_schema", "handler_type"],
269
343
  },
270
344
  },
271
345
  {
272
- name: "unassign_mcp_server_from_agent",
273
- description: "Remove an MCP server from an agent.",
346
+ name: "update_tool_on_local_server",
347
+ description: `Update an existing tool on a local MCP server. Only provide fields you want to change. Only works for servers with type "local".`,
274
348
  inputSchema: {
275
349
  type: "object",
276
350
  properties: {
277
- agent_id: { type: "string", description: "The agent ID" },
278
- server_id: { type: "string", description: "The MCP server ID to remove" },
351
+ tool_id: { type: "string", description: "The tool ID to update" },
352
+ name: { type: "string", description: "New tool name" },
353
+ description: { type: "string", description: "New description" },
354
+ input_schema: { type: "object", description: "New JSON Schema for tool parameters" },
355
+ handler_type: { type: "string", description: "New handler type: mock, http, or javascript" },
356
+ mock_response: { type: "object", description: "New mock response template" },
357
+ http_config: { type: "object", description: "New HTTP config: { method, url, headers?, body? }" },
358
+ code: { type: "string", description: "New JavaScript code" },
359
+ enabled: { type: "boolean", description: "Enable or disable the tool" },
279
360
  },
280
- required: ["agent_id", "server_id"],
361
+ required: ["tool_id"],
281
362
  },
282
363
  },
283
364
  {
@@ -300,14 +381,69 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
300
381
  required: ["agent_id", "message"],
301
382
  },
302
383
  },
384
+ // Task management on agents
385
+ {
386
+ name: "list_agent_tasks",
387
+ description: "List tasks on a running agent. Tasks are scheduled work items that agents execute autonomously.",
388
+ inputSchema: {
389
+ type: "object",
390
+ properties: {
391
+ agent_id: { type: "string", description: "The agent ID to list tasks from" },
392
+ status: { type: "string", description: "Filter by status: all, pending, running, completed, failed, cancelled (default: all)" },
393
+ },
394
+ required: ["agent_id"],
395
+ },
396
+ },
397
+ {
398
+ name: "create_agent_task",
399
+ description: "Create a new task on a running agent. The agent must have the tasks feature enabled.",
400
+ inputSchema: {
401
+ type: "object",
402
+ properties: {
403
+ agent_id: { type: "string", description: "The agent ID to create the task on" },
404
+ title: { type: "string", description: "Task title" },
405
+ description: { type: "string", description: "Task description / instructions for the agent" },
406
+ type: { type: "string", description: "Task type: once (one-time) or recurring (default: once)" },
407
+ priority: { type: "number", description: "Priority 1-10, higher = more important (default: 5)" },
408
+ execute_at: { type: "string", description: "ISO timestamp for scheduled execution (optional, for one-time tasks)" },
409
+ recurrence: { type: "string", description: "Cron expression for recurring tasks (e.g. '*/30 * * * *' = every 30 min)" },
410
+ },
411
+ required: ["agent_id", "title"],
412
+ },
413
+ },
414
+ {
415
+ name: "delete_agent_task",
416
+ description: "Delete a task from a running agent.",
417
+ inputSchema: {
418
+ type: "object",
419
+ properties: {
420
+ agent_id: { type: "string", description: "The agent ID" },
421
+ task_id: { type: "string", description: "The task ID to delete" },
422
+ },
423
+ required: ["agent_id", "task_id"],
424
+ },
425
+ },
426
+ {
427
+ name: "execute_agent_task",
428
+ description: "Immediately execute a task on a running agent, regardless of its schedule.",
429
+ inputSchema: {
430
+ type: "object",
431
+ properties: {
432
+ agent_id: { type: "string", description: "The agent ID" },
433
+ task_id: { type: "string", description: "The task ID to execute" },
434
+ },
435
+ required: ["agent_id", "task_id"],
436
+ },
437
+ },
303
438
  // Skills management
304
439
  {
305
440
  name: "list_skills",
306
- description: "List all installed skills. Skills are reusable instruction sets (like prompt templates with tool permissions) that give agents specialized capabilities. Skills can be installed from the SkillsMP marketplace or created locally.",
441
+ description: "List all installed skills. Skills are reusable instruction sets (like prompt templates with tool permissions) that give agents specialized capabilities. Skills can be installed from the SkillsMP marketplace or created locally. Pass project_id to only see skills scoped to that project (plus global skills).",
307
442
  inputSchema: {
308
443
  type: "object",
309
444
  properties: {
310
445
  enabled_only: { type: "boolean", description: "Only return enabled skills (optional, default false)" },
446
+ project_id: { type: "string", description: "Filter by project ID — returns skills scoped to this project plus global skills" },
311
447
  },
312
448
  },
313
449
  },
@@ -335,27 +471,22 @@ After creating, assign to agents with assign_mcp_server_to_agent. HTTP servers w
335
471
  },
336
472
  },
337
473
  {
338
- name: "assign_skill_to_agent",
339
- description: "Assign a skill to an agent. The skill's instructions and tool permissions will be pushed to the agent on next start/restart.",
474
+ name: "update_skill",
475
+ description: "Update an existing skill. Only provide fields you want to change.",
340
476
  inputSchema: {
341
477
  type: "object",
342
478
  properties: {
343
- agent_id: { type: "string", description: "The agent ID" },
344
- skill_id: { type: "string", description: "The skill ID to assign" },
345
- },
346
- required: ["agent_id", "skill_id"],
347
- },
348
- },
349
- {
350
- name: "unassign_skill_from_agent",
351
- description: "Remove a skill from an agent.",
352
- inputSchema: {
353
- type: "object",
354
- properties: {
355
- agent_id: { type: "string", description: "The agent ID" },
356
- skill_id: { type: "string", description: "The skill ID to remove" },
479
+ skill_id: { type: "string", description: "The skill ID to update" },
480
+ name: { type: "string", description: "New name" },
481
+ description: { type: "string", description: "New description" },
482
+ content: { type: "string", description: "New instructions content (markdown)" },
483
+ allowed_tools: {
484
+ type: "array",
485
+ items: { type: "string" },
486
+ description: "New list of allowed MCP tool names",
487
+ },
357
488
  },
358
- required: ["agent_id", "skill_id"],
489
+ required: ["skill_id"],
359
490
  },
360
491
  },
361
492
  {
@@ -490,18 +621,6 @@ Use this to find trigger slugs before creating a subscription.`,
490
621
  required: ["provider"],
491
622
  },
492
623
  },
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
624
  {
506
625
  name: "list_subscriptions",
507
626
  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.",
@@ -515,22 +634,21 @@ Use this to find trigger slugs before creating a subscription.`,
515
634
  },
516
635
  {
517
636
  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.
637
+ description: `Create a trigger subscription: registers a webhook with the external service and routes events to an agent. The webhook URL is auto-configured — you do NOT need to provide a callback URL.
519
638
 
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
639
+ Just provide all 4 required fields in a single call:
640
+ - provider: "agentdojo" or "composio"
641
+ - trigger_slug: from list_trigger_types
642
+ - connected_account_id: from list_integration_connections
643
+ - agent_id: from list_agents
526
644
 
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.`,
645
+ Some triggers require extra config fields (e.g. GitHub needs "owner" and "repo") pass them in the config object.`,
528
646
  inputSchema: {
529
647
  type: "object",
530
648
  properties: {
531
649
  provider: { type: "string", description: "Trigger provider ID: composio or agentdojo" },
532
650
  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." },
651
+ connected_account_id: { type: "string", description: "Connected account ID that owns the integration. Use list_integration_connections to find IDs." },
534
652
  agent_id: { type: "string", description: "Agent ID to route trigger events to. Use list_agents to find IDs." },
535
653
  project_id: { type: "string", description: "Project ID for scoping (optional)" },
536
654
  config: {
@@ -581,8 +699,164 @@ IMPORTANT: Some triggers require extra config fields (e.g. GitHub triggers need
581
699
  required: ["subscription_id"],
582
700
  },
583
701
  },
702
+ // Integration management tools
703
+ {
704
+ name: "list_integration_providers",
705
+ description: "List available integration providers (e.g. agentdojo, composio) and whether they have API keys configured. Integration providers give access to third-party apps, OAuth connections, and MCP server creation.",
706
+ inputSchema: {
707
+ type: "object",
708
+ properties: {
709
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
710
+ },
711
+ },
712
+ },
713
+ {
714
+ name: "list_integration_apps",
715
+ description: `List available apps/toolkits from an integration provider. Each app represents a service (e.g. GitHub, Slack, Stripe) that can be connected via OAuth or API key.
716
+
717
+ Returns apps with their auth schemes (OAUTH2, API_KEY), connection status, and categories.
718
+ Use this to browse what's available before connecting an app.`,
719
+ inputSchema: {
720
+ type: "object",
721
+ properties: {
722
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
723
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
724
+ search: { type: "string", description: "Optional search filter for app name/slug" },
725
+ },
726
+ required: ["provider"],
727
+ },
728
+ },
729
+ {
730
+ name: "connect_integration_app",
731
+ description: `Connect/authenticate an app by storing credentials on the integration provider. This enables the app's tools to be used in MCP servers.
732
+
733
+ NOTE: Only API_KEY auth is supported from the assistant. For OAuth apps, direct the user to the Browse Toolkits UI.
734
+
735
+ Some apps require multiple credential fields (e.g. Pushover needs appToken + userKey). Use list_integration_apps to see credentialFields for each app — if present, pass all required fields in the "credentials" object. If the app has no credentialFields, pass a single "api_key".
736
+
737
+ WORKFLOW:
738
+ 1. list_integration_apps to find the app and its credentialFields
739
+ 2. connect_integration_app with the app slug and credentials
740
+ 3. create_integration_config to create an MCP server from the connected app
741
+ 4. add_integration_config_locally to add it as a local MCP server`,
742
+ inputSchema: {
743
+ type: "object",
744
+ properties: {
745
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
746
+ app_slug: { type: "string", description: "App slug (from list_integration_apps)" },
747
+ api_key: { type: "string", description: "Single API key (for apps with no credentialFields)" },
748
+ credentials: {
749
+ type: "object",
750
+ description: "Credential fields as key-value pairs (e.g. { appToken: '...', userKey: '...' }). Use this for apps with multiple credentialFields. Takes priority over api_key.",
751
+ },
752
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
753
+ },
754
+ required: ["provider", "app_slug"],
755
+ },
756
+ },
757
+ {
758
+ name: "list_integration_connections",
759
+ description: "List connected accounts (credentials) for an integration provider. Shows which apps have been authenticated and their status.",
760
+ inputSchema: {
761
+ type: "object",
762
+ properties: {
763
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
764
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
765
+ },
766
+ required: ["provider"],
767
+ },
768
+ },
769
+ {
770
+ name: "disconnect_integration_app",
771
+ description: "Disconnect/revoke a connected account (credential) from an integration provider.",
772
+ inputSchema: {
773
+ type: "object",
774
+ properties: {
775
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
776
+ connection_id: { type: "string", description: "Connection/credential ID (from list_integration_connections)" },
777
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
778
+ },
779
+ required: ["provider", "connection_id"],
780
+ },
781
+ },
782
+ {
783
+ name: "list_integration_configs",
784
+ description: "List MCP server configs on an integration provider. These are remote MCP servers hosted by the provider that can be added locally.",
785
+ inputSchema: {
786
+ type: "object",
787
+ properties: {
788
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
789
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
790
+ },
791
+ required: ["provider"],
792
+ },
793
+ },
794
+ {
795
+ name: "create_integration_config",
796
+ description: `Create an MCP server config on an integration provider from a connected app/toolkit. This creates a remote MCP server on the provider that bundles the app's tools.
797
+
798
+ After creation, use add_integration_config_locally to add it as a local MCP server.
799
+
800
+ WORKFLOW:
801
+ 1. list_integration_apps → find the app slug
802
+ 2. Ensure app is connected (list_integration_connections or connect_integration_app)
803
+ 3. create_integration_config → creates remote MCP server
804
+ 4. add_integration_config_locally → adds it locally so agents can use it`,
805
+ inputSchema: {
806
+ type: "object",
807
+ properties: {
808
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
809
+ name: { type: "string", description: "Name for the MCP config (e.g. 'GitHub MCP', 'Slack Tools')" },
810
+ toolkit_slug: { type: "string", description: "Toolkit/app slug to create the config from" },
811
+ project_id: { type: "string", description: "Project ID for project-scoped API keys (optional)" },
812
+ },
813
+ required: ["provider", "name", "toolkit_slug"],
814
+ },
815
+ },
816
+ {
817
+ name: "add_integration_config_locally",
818
+ description: `Add a remote integration MCP config as a local MCP server. This creates a local MCP server entry that connects to the provider's hosted MCP endpoint, making its tools available to agents.
819
+
820
+ After adding, use assign_mcp_server_to_agent to give an agent access to these tools.`,
821
+ inputSchema: {
822
+ type: "object",
823
+ properties: {
824
+ provider: { type: "string", description: "Integration provider ID: agentdojo or composio" },
825
+ config_id: { type: "string", description: "Config/server ID on the provider (from list_integration_configs or create_integration_config)" },
826
+ project_id: { type: "string", description: "Project ID to scope the local server to (optional)" },
827
+ },
828
+ required: ["provider", "config_id"],
829
+ },
830
+ },
584
831
  ];
585
832
 
833
+ // Project-only tools — hidden entirely when PROJECTS_ENABLED is not set
834
+ const PROJECT_ONLY_TOOLS = new Set(["list_projects", "create_project", "update_project", "delete_project"]);
835
+
836
+ // Build tools list — when PROJECTS_ENABLED, add project_id to required for all tools that accept it
837
+ function getPlatformTools() {
838
+ const projectsEnabled = process.env.PROJECTS_ENABLED === "true";
839
+ if (!projectsEnabled) {
840
+ return PLATFORM_TOOLS.filter(tool => !PROJECT_ONLY_TOOLS.has(tool.name));
841
+ }
842
+
843
+ return PLATFORM_TOOLS.map(tool => {
844
+ const props = tool.inputSchema.properties as Record<string, unknown> | undefined;
845
+ if (!props || !("project_id" in props)) return tool;
846
+
847
+ const existing = (tool.inputSchema as any).required || [];
848
+ if (existing.includes("project_id")) return tool;
849
+
850
+ return {
851
+ ...tool,
852
+ inputSchema: {
853
+ ...tool.inputSchema,
854
+ required: [...existing, "project_id"],
855
+ },
856
+ };
857
+ });
858
+ }
859
+
586
860
  // Tool execution handlers
587
861
  async function executeTool(name: string, args: Record<string, any>): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
588
862
  try {
@@ -662,8 +936,46 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
662
936
  updates.features = { ...agent.features, ...args.features };
663
937
  }
664
938
 
939
+ // Skills: set, add, or remove
940
+ let skills = agent.skills || [];
941
+ if (args.skill_ids !== undefined) {
942
+ skills = args.skill_ids;
943
+ }
944
+ if (args.add_skills) {
945
+ for (const sid of args.add_skills) {
946
+ if (!skills.includes(sid)) skills.push(sid);
947
+ }
948
+ }
949
+ if (args.remove_skills) {
950
+ skills = skills.filter((id: string) => !args.remove_skills.includes(id));
951
+ }
952
+ if (args.skill_ids !== undefined || args.add_skills || args.remove_skills) {
953
+ updates.skills = skills;
954
+ }
955
+
956
+ // MCP servers: set, add, or remove
957
+ let mcpServers = agent.mcp_servers || [];
958
+ if (args.mcp_server_ids !== undefined) {
959
+ mcpServers = args.mcp_server_ids;
960
+ }
961
+ if (args.add_mcp_servers) {
962
+ for (const sid of args.add_mcp_servers) {
963
+ if (!mcpServers.includes(sid)) mcpServers.push(sid);
964
+ }
965
+ }
966
+ if (args.remove_mcp_servers) {
967
+ mcpServers = mcpServers.filter((id: string) => !args.remove_mcp_servers.includes(id));
968
+ }
969
+ if (args.mcp_server_ids !== undefined || args.add_mcp_servers || args.remove_mcp_servers) {
970
+ updates.mcp_servers = mcpServers;
971
+ // Auto-enable MCP feature if servers are being added
972
+ if (mcpServers.length > 0 && !agent.features.mcp) {
973
+ updates.features = { ...(updates.features || agent.features), mcp: true };
974
+ }
975
+ }
976
+
665
977
  const updated = AgentDB.update(args.agent_id, updates);
666
- return { content: [{ type: "text", text: `Agent updated: ${JSON.stringify({ id: updated?.id, name: updated?.name }, null, 2)}` }] };
978
+ return { content: [{ type: "text", text: `Agent updated: ${JSON.stringify({ id: updated?.id, name: updated?.name, skills: updates.skills !== undefined ? updates.skills.length + " skills" : "unchanged", mcp_servers: updates.mcp_servers !== undefined ? updates.mcp_servers.length + " servers" : "unchanged" }, null, 2)}` }] };
667
979
  }
668
980
 
669
981
  case "delete_agent": {
@@ -740,6 +1052,36 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
740
1052
  return { content: [{ type: "text", text: `Project created: ${JSON.stringify({ id: project.id, name: project.name, color: project.color }, null, 2)}` }] };
741
1053
  }
742
1054
 
1055
+ case "update_project": {
1056
+ const project = ProjectDB.findById(args.project_id);
1057
+ if (!project) return { content: [{ type: "text", text: `Project not found: ${args.project_id}` }], isError: true };
1058
+
1059
+ const updates: Record<string, unknown> = {};
1060
+ if (args.name !== undefined) updates.name = args.name;
1061
+ if (args.description !== undefined) updates.description = args.description;
1062
+ if (args.color !== undefined) updates.color = args.color;
1063
+
1064
+ if (Object.keys(updates).length === 0) {
1065
+ return { content: [{ type: "text", text: "No updates provided" }], isError: true };
1066
+ }
1067
+
1068
+ const updated = ProjectDB.update(args.project_id, updates);
1069
+ return { content: [{ type: "text", text: `Project updated: ${JSON.stringify({ id: updated!.id, name: updated!.name, description: updated!.description, color: updated!.color }, null, 2)}` }] };
1070
+ }
1071
+
1072
+ case "delete_project": {
1073
+ const project = ProjectDB.findById(args.project_id);
1074
+ if (!project) return { content: [{ type: "text", text: `Project not found: ${args.project_id}` }], isError: true };
1075
+
1076
+ const agentCounts = ProjectDB.getAgentCounts();
1077
+ const agentCount = agentCounts.get(args.project_id) || 0;
1078
+
1079
+ const deleted = ProjectDB.delete(args.project_id);
1080
+ if (!deleted) return { content: [{ type: "text", text: "Failed to delete project" }], isError: true };
1081
+
1082
+ return { content: [{ type: "text", text: `Project "${project.name}" deleted. ${agentCount} agent(s) were unassigned.` }] };
1083
+ }
1084
+
743
1085
  case "list_providers": {
744
1086
  const providers = getProvidersWithStatus();
745
1087
  const llmProviders = providers.filter(p => p.type === "llm");
@@ -754,8 +1096,8 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
754
1096
 
755
1097
  case "list_mcp_servers": {
756
1098
  const servers = args.project_id
757
- ? McpServerDB.findByProject(args.project_id)
758
- : McpServerDB.findAll();
1099
+ ? McpServerDB.findByProjectLight(args.project_id)
1100
+ : McpServerDB.findAllLight();
759
1101
  const result = servers.map(s => ({
760
1102
  id: s.id,
761
1103
  name: s.name,
@@ -773,7 +1115,7 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
773
1115
  if (!server) {
774
1116
  return { content: [{ type: "text", text: `MCP server not found: ${args.server_id}` }], isError: true };
775
1117
  }
776
- return { content: [{ type: "text", text: JSON.stringify({
1118
+ const serverInfo: Record<string, any> = {
777
1119
  id: server.id,
778
1120
  name: server.name,
779
1121
  type: server.type,
@@ -785,7 +1127,23 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
785
1127
  port: server.port,
786
1128
  source: server.source,
787
1129
  projectId: server.project_id,
788
- }, null, 2) }] };
1130
+ };
1131
+ // Include tools with full details for local servers
1132
+ if (server.type === "local") {
1133
+ const tools = McpServerToolDB.findByServer(server.id);
1134
+ serverInfo.tools = tools.map(t => ({
1135
+ id: t.id,
1136
+ name: t.name,
1137
+ description: t.description,
1138
+ handler_type: t.handler_type,
1139
+ enabled: t.enabled,
1140
+ input_schema: t.input_schema,
1141
+ mock_response: t.mock_response,
1142
+ http_config: t.http_config,
1143
+ code: t.code,
1144
+ }));
1145
+ }
1146
+ return { content: [{ type: "text", text: JSON.stringify(serverInfo, null, 2) }] };
789
1147
  }
790
1148
 
791
1149
  case "create_mcp_server": {
@@ -819,38 +1177,57 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
819
1177
  return { content: [{ type: "text", text: `MCP server deleted: ${server.name} (${server.id})` }] };
820
1178
  }
821
1179
 
822
- case "assign_mcp_server_to_agent": {
823
- const agent = AgentDB.findById(args.agent_id);
824
- if (!agent) {
825
- return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
826
- }
1180
+ case "add_tool_to_local_server": {
827
1181
  const server = McpServerDB.findById(args.server_id);
828
1182
  if (!server) {
829
1183
  return { content: [{ type: "text", text: `MCP server not found: ${args.server_id}` }], isError: true };
830
1184
  }
831
- const mcpServers = agent.mcp_servers || [];
832
- if (mcpServers.includes(args.server_id)) {
833
- return { content: [{ type: "text", text: `Server ${server.name} is already assigned to ${agent.name}` }] };
1185
+ if (server.type !== "local") {
1186
+ return { content: [{ type: "text", text: `Server "${server.name}" is type "${server.type}" — add_tool_to_local_server only works for local servers.` }], isError: true };
834
1187
  }
835
- AgentDB.update(args.agent_id, { mcp_servers: [...mcpServers, args.server_id] });
836
- // Enable MCP feature if not already
837
- if (!agent.features.mcp) {
838
- AgentDB.update(args.agent_id, { features: { ...agent.features, mcp: true } });
1188
+ // Check for duplicate tool name
1189
+ const existing = McpServerToolDB.findByServerAndName(args.server_id, args.name);
1190
+ if (existing) {
1191
+ return { content: [{ type: "text", text: `Tool "${args.name}" already exists on this server (ID: ${existing.id}). Use a different name or delete the existing tool first.` }], isError: true };
839
1192
  }
840
- return { content: [{ type: "text", text: `Assigned MCP server "${server.name}" to agent "${agent.name}". Restart the agent for changes to take effect.` }] };
1193
+ const tool = McpServerToolDB.create({
1194
+ id: generateId(),
1195
+ server_id: args.server_id,
1196
+ name: args.name,
1197
+ description: args.description,
1198
+ input_schema: args.input_schema || { type: "object", properties: {} },
1199
+ handler_type: args.handler_type || "mock",
1200
+ mock_response: args.mock_response || null,
1201
+ http_config: args.http_config || null,
1202
+ code: args.code || null,
1203
+ enabled: true,
1204
+ });
1205
+ return { content: [{ type: "text", text: `Tool added: ${JSON.stringify({ id: tool.id, name: tool.name, handler_type: tool.handler_type, server: server.name }, null, 2)}` }] };
841
1206
  }
842
1207
 
843
- case "unassign_mcp_server_from_agent": {
844
- const agent = AgentDB.findById(args.agent_id);
845
- if (!agent) {
846
- return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1208
+ case "update_tool_on_local_server": {
1209
+ const tool = McpServerToolDB.findById(args.tool_id);
1210
+ if (!tool) {
1211
+ return { content: [{ type: "text", text: `Tool not found: ${args.tool_id}` }], isError: true };
1212
+ }
1213
+ const server = McpServerDB.findById(tool.server_id);
1214
+ if (!server || server.type !== "local") {
1215
+ return { content: [{ type: "text", text: `Tool belongs to a non-local server — update_tool_on_local_server only works for local servers.` }], isError: true };
847
1216
  }
848
- const mcpServers = agent.mcp_servers || [];
849
- if (!mcpServers.includes(args.server_id)) {
850
- return { content: [{ type: "text", text: `Server is not assigned to this agent` }] };
1217
+ const updates: Record<string, unknown> = {};
1218
+ if (args.name !== undefined) updates.name = args.name;
1219
+ if (args.description !== undefined) updates.description = args.description;
1220
+ if (args.input_schema !== undefined) updates.input_schema = args.input_schema;
1221
+ if (args.handler_type !== undefined) updates.handler_type = args.handler_type;
1222
+ if (args.mock_response !== undefined) updates.mock_response = args.mock_response;
1223
+ if (args.http_config !== undefined) updates.http_config = args.http_config;
1224
+ if (args.code !== undefined) updates.code = args.code;
1225
+ if (args.enabled !== undefined) updates.enabled = args.enabled;
1226
+ const updated = McpServerToolDB.update(args.tool_id, updates);
1227
+ if (!updated) {
1228
+ return { content: [{ type: "text", text: `Failed to update tool ${args.tool_id}` }], isError: true };
851
1229
  }
852
- AgentDB.update(args.agent_id, { mcp_servers: mcpServers.filter((id: string) => id !== args.server_id) });
853
- return { content: [{ type: "text", text: `Removed MCP server from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
1230
+ return { content: [{ type: "text", text: `Tool updated: ${JSON.stringify({ id: updated.id, name: updated.name, handler_type: updated.handler_type, enabled: updated.enabled, server: server.name }, null, 2)}` }] };
854
1231
  }
855
1232
 
856
1233
  case "get_dashboard_stats": {
@@ -859,7 +1236,7 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
859
1236
  const projectCount = ProjectDB.count();
860
1237
  const providers = getProvidersWithStatus().filter(p => p.type === "llm");
861
1238
  const configuredProviders = providers.filter(p => p.hasKey).length;
862
- const mcpServerCount = McpServerDB.findAll().length;
1239
+ const mcpServerCount = McpServerDB.count();
863
1240
  const skillCount = SkillDB.count();
864
1241
 
865
1242
  return {
@@ -890,7 +1267,7 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
890
1267
  method: "POST",
891
1268
  headers: { "Content-Type": "application/json" },
892
1269
  body: JSON.stringify({ message: args.message }),
893
- signal: AbortSignal.timeout(60000),
1270
+ signal: AbortSignal.timeout(120000),
894
1271
  });
895
1272
 
896
1273
  if (!res.ok) {
@@ -898,16 +1275,129 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
898
1275
  return { content: [{ type: "text", text: `Agent responded with error: ${err}` }], isError: true };
899
1276
  }
900
1277
 
901
- const data = await res.json();
902
- const reply = data.response || data.message || JSON.stringify(data);
1278
+ // Agent returns SSE stream — consume and assemble content chunks
1279
+ const reader = res.body?.getReader();
1280
+ if (!reader) {
1281
+ return { content: [{ type: "text", text: "No response stream from agent" }], isError: true };
1282
+ }
1283
+
1284
+ const decoder = new TextDecoder();
1285
+ const contentChunks: string[] = [];
1286
+ let raw = "";
1287
+
1288
+ while (true) {
1289
+ const { done, value } = await reader.read();
1290
+ if (done) break;
1291
+ raw += decoder.decode(value, { stream: true });
1292
+ }
1293
+
1294
+ // Parse SSE lines: "data: {...}"
1295
+ for (const line of raw.split("\n")) {
1296
+ const sseData = line.startsWith("data: ") ? line.slice(6) : line.trim();
1297
+ if (!sseData) continue;
1298
+ try {
1299
+ const evt = JSON.parse(sseData);
1300
+ if (evt.type === "content" && evt.content) {
1301
+ contentChunks.push(evt.content);
1302
+ }
1303
+ } catch {
1304
+ // Not valid JSON — skip
1305
+ }
1306
+ }
1307
+
1308
+ const reply = contentChunks.join("") || "(no response)";
903
1309
  return { content: [{ type: "text", text: reply }] };
904
1310
  } catch (err) {
905
1311
  return { content: [{ type: "text", text: `Failed to communicate with agent: ${err}` }], isError: true };
906
1312
  }
907
1313
  }
908
1314
 
1315
+ case "list_agent_tasks": {
1316
+ const agent = AgentDB.findById(args.agent_id);
1317
+ if (!agent) return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1318
+ if (agent.status !== "running" || !agent.port) return { content: [{ type: "text", text: `Agent ${agent.name} is not running` }], isError: true };
1319
+
1320
+ const status = args.status || "all";
1321
+ const data = await fetchFromAgent(agent.id, agent.port, `/tasks?status=${status}`);
1322
+ if (!data) return { content: [{ type: "text", text: "Failed to fetch tasks from agent" }], isError: true };
1323
+
1324
+ const tasks = (data.tasks || []).map((t: any) => ({
1325
+ id: t.id, title: t.title, type: t.type, status: t.status, priority: t.priority,
1326
+ recurrence: t.recurrence, next_run: t.next_run, execute_at: t.execute_at, created_at: t.created_at,
1327
+ }));
1328
+ return { content: [{ type: "text", text: JSON.stringify({ tasks, count: tasks.length }, null, 2) }] };
1329
+ }
1330
+
1331
+ case "create_agent_task": {
1332
+ const agent = AgentDB.findById(args.agent_id);
1333
+ if (!agent) return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1334
+ if (agent.status !== "running" || !agent.port) return { content: [{ type: "text", text: `Agent ${agent.name} is not running` }], isError: true };
1335
+
1336
+ const body: Record<string, unknown> = {
1337
+ title: args.title,
1338
+ description: args.description,
1339
+ type: args.type || "once",
1340
+ priority: args.priority || 5,
1341
+ };
1342
+ if (args.execute_at) body.execute_at = args.execute_at;
1343
+ if (args.recurrence) body.recurrence = args.recurrence;
1344
+
1345
+ try {
1346
+ const res = await agentFetch(agent.id, agent.port, "/tasks", {
1347
+ method: "POST",
1348
+ headers: { "Content-Type": "application/json" },
1349
+ body: JSON.stringify(body),
1350
+ signal: AbortSignal.timeout(5000),
1351
+ });
1352
+ const data = await res.json();
1353
+ if (!res.ok) return { content: [{ type: "text", text: `Failed to create task: ${data.error || res.status}` }], isError: true };
1354
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1355
+ } catch (err) {
1356
+ return { content: [{ type: "text", text: `Failed to create task: ${err}` }], isError: true };
1357
+ }
1358
+ }
1359
+
1360
+ case "delete_agent_task": {
1361
+ const agent = AgentDB.findById(args.agent_id);
1362
+ if (!agent) return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1363
+ if (agent.status !== "running" || !agent.port) return { content: [{ type: "text", text: `Agent ${agent.name} is not running` }], isError: true };
1364
+
1365
+ try {
1366
+ const res = await agentFetch(agent.id, agent.port, `/tasks/${args.task_id}`, {
1367
+ method: "DELETE",
1368
+ signal: AbortSignal.timeout(5000),
1369
+ });
1370
+ const data = await res.json();
1371
+ if (!res.ok) return { content: [{ type: "text", text: `Failed to delete task: ${data.error || res.status}` }], isError: true };
1372
+ return { content: [{ type: "text", text: `Task ${args.task_id} deleted successfully` }] };
1373
+ } catch (err) {
1374
+ return { content: [{ type: "text", text: `Failed to delete task: ${err}` }], isError: true };
1375
+ }
1376
+ }
1377
+
1378
+ case "execute_agent_task": {
1379
+ const agent = AgentDB.findById(args.agent_id);
1380
+ if (!agent) return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
1381
+ if (agent.status !== "running" || !agent.port) return { content: [{ type: "text", text: `Agent ${agent.name} is not running` }], isError: true };
1382
+
1383
+ try {
1384
+ const res = await agentFetch(agent.id, agent.port, `/tasks/${args.task_id}/execute`, {
1385
+ method: "POST",
1386
+ signal: AbortSignal.timeout(5000),
1387
+ });
1388
+ const data = await res.json();
1389
+ if (!res.ok) return { content: [{ type: "text", text: `Failed to execute task: ${data.error || res.status}` }], isError: true };
1390
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1391
+ } catch (err) {
1392
+ return { content: [{ type: "text", text: `Failed to execute task: ${err}` }], isError: true };
1393
+ }
1394
+ }
1395
+
909
1396
  case "list_skills": {
910
- const skills = args.enabled_only ? SkillDB.findEnabled() : SkillDB.findAll();
1397
+ let skills = args.enabled_only ? SkillDB.findEnabled() : SkillDB.findAll();
1398
+ if (args.project_id) {
1399
+ skills = skills.filter(s => !s.project_id || s.project_id === args.project_id);
1400
+ }
911
1401
  const result = skills.map(s => ({
912
1402
  id: s.id,
913
1403
  name: s.name,
@@ -947,34 +1437,18 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
947
1437
  return { content: [{ type: "text", text: `Skill "${skill.name}" ${args.enabled ? "enabled" : "disabled"}` }] };
948
1438
  }
949
1439
 
950
- case "assign_skill_to_agent": {
951
- const agent = AgentDB.findById(args.agent_id);
952
- if (!agent) {
953
- return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
954
- }
1440
+ case "update_skill": {
955
1441
  const skill = SkillDB.findById(args.skill_id);
956
1442
  if (!skill) {
957
1443
  return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }], isError: true };
958
1444
  }
959
- const skills = agent.skills || [];
960
- if (skills.includes(args.skill_id)) {
961
- return { content: [{ type: "text", text: `Skill "${skill.name}" is already assigned to "${agent.name}"` }] };
962
- }
963
- AgentDB.update(args.agent_id, { skills: [...skills, args.skill_id] });
964
- return { content: [{ type: "text", text: `Assigned skill "${skill.name}" to agent "${agent.name}". Restart the agent for changes to take effect.` }] };
965
- }
966
-
967
- case "unassign_skill_from_agent": {
968
- const agent = AgentDB.findById(args.agent_id);
969
- if (!agent) {
970
- return { content: [{ type: "text", text: `Agent not found: ${args.agent_id}` }], isError: true };
971
- }
972
- const skills = agent.skills || [];
973
- if (!skills.includes(args.skill_id)) {
974
- return { content: [{ type: "text", text: `Skill is not assigned to this agent` }] };
975
- }
976
- AgentDB.update(args.agent_id, { skills: skills.filter((id: string) => id !== args.skill_id) });
977
- return { content: [{ type: "text", text: `Removed skill from agent "${agent.name}". Restart the agent for changes to take effect.` }] };
1445
+ const updates: Record<string, any> = {};
1446
+ if (args.name !== undefined) updates.name = args.name;
1447
+ if (args.description !== undefined) updates.description = args.description;
1448
+ if (args.content !== undefined) updates.content = args.content;
1449
+ if (args.allowed_tools !== undefined) updates.allowed_tools = args.allowed_tools;
1450
+ const updated = SkillDB.update(args.skill_id, updates);
1451
+ return { content: [{ type: "text", text: `Skill "${updated?.name || skill.name}" updated.` }] };
978
1452
  }
979
1453
 
980
1454
  case "create_skill": {
@@ -1138,26 +1612,6 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
1138
1612
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1139
1613
  }
1140
1614
 
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
1615
 
1162
1616
  case "list_subscriptions": {
1163
1617
  let subscriptions;
@@ -1218,9 +1672,11 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
1218
1672
  }
1219
1673
  }
1220
1674
 
1221
- // Create remote trigger on the provider
1675
+ // Create remote trigger on the provider — auto-inject callback_url
1676
+ const webhookUrl = SettingsDB.get(`${providerId}_webhook_url`);
1222
1677
  const config: Record<string, unknown> = {
1223
1678
  agent_id: args.agent_id,
1679
+ ...(webhookUrl ? { callback_url: webhookUrl } : {}),
1224
1680
  ...(args.config || {}),
1225
1681
  };
1226
1682
  const triggerResult = await triggerProvider.createTrigger(apiKey, args.trigger_slug, args.connected_account_id, config);
@@ -1313,6 +1769,272 @@ async function executeTool(name: string, args: Record<string, any>): Promise<{ c
1313
1769
  return { content: [{ type: "text", text: `Subscription "${sub.trigger_slug}" deleted` }] };
1314
1770
  }
1315
1771
 
1772
+ // Integration management tools
1773
+ case "list_integration_providers": {
1774
+
1775
+ const providerIds = getProviderIds();
1776
+ const projectId = args.project_id || null;
1777
+ const result = providerIds.map(id => {
1778
+ const provider = getProvider(id);
1779
+ const hasKey = !!ProviderKeys.getDecryptedForProject(id, projectId);
1780
+ return { id, name: provider?.name || id, hasKey };
1781
+ });
1782
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1783
+ }
1784
+
1785
+ case "list_integration_apps": {
1786
+
1787
+ const providerId = args.provider;
1788
+ const projectId = args.project_id || null;
1789
+ const provider = getProvider(providerId);
1790
+ if (!provider) {
1791
+ return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}. Available: ${getProviderIds().join(", ")}` }], isError: true };
1792
+ }
1793
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1794
+ if (!apiKey) {
1795
+ return { content: [{ type: "text", text: `${provider.name} API key not configured` }], isError: true };
1796
+ }
1797
+ let apps = await provider.listApps(apiKey);
1798
+ if (args.search) {
1799
+ const s = args.search.toLowerCase();
1800
+ apps = apps.filter(a =>
1801
+ a.name.toLowerCase().includes(s) ||
1802
+ a.slug.toLowerCase().includes(s) ||
1803
+ a.description?.toLowerCase().includes(s)
1804
+ );
1805
+ }
1806
+ // Also check which are connected
1807
+ let connectedIds = new Set<string>();
1808
+ try {
1809
+ const accounts = await provider.listConnectedAccounts(apiKey, "platform-agent");
1810
+ connectedIds = new Set(accounts.filter(a => a.status === "active").map(a => a.appId));
1811
+ } catch {}
1812
+ const result = apps.slice(0, 50).map(a => ({
1813
+ slug: a.slug,
1814
+ name: a.name,
1815
+ description: a.description,
1816
+ authSchemes: a.authSchemes,
1817
+ categories: a.categories,
1818
+ connected: connectedIds.has(a.slug) || (a.providerSlug ? connectedIds.has(a.providerSlug) : false),
1819
+ credentialFields: a.credentialFields || undefined,
1820
+ }));
1821
+ return { content: [{ type: "text", text: `Found ${apps.length} apps (showing ${result.length}):\n${JSON.stringify(result, null, 2)}` }] };
1822
+ }
1823
+
1824
+ case "connect_integration_app": {
1825
+ const providerId = args.provider;
1826
+ const projectId = args.project_id || null;
1827
+ const provider = getProvider(providerId);
1828
+ if (!provider) {
1829
+ return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}` }], isError: true };
1830
+ }
1831
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1832
+ if (!apiKey) {
1833
+ return { content: [{ type: "text", text: `${provider.name} API key not configured` }], isError: true };
1834
+ }
1835
+ if (!args.api_key && !args.credentials) {
1836
+ return { content: [{ type: "text", text: `Either api_key or credentials is required` }], isError: true };
1837
+ }
1838
+ // Build credential object — prefer multi-field credentials over single api_key
1839
+ const creds: any = { authScheme: "API_KEY" as const };
1840
+ if (args.credentials && Object.keys(args.credentials).length > 0) {
1841
+ creds.fields = args.credentials;
1842
+ } else {
1843
+ creds.apiKey = args.api_key;
1844
+ }
1845
+ const connectionResult = await provider.initiateConnection(apiKey, "platform-agent", args.app_slug, "", creds);
1846
+ if (connectionResult.status === "active") {
1847
+ return { content: [{ type: "text", text: `Successfully connected "${args.app_slug}". Connection ID: ${connectionResult.connectionId || "N/A"}` }] };
1848
+ }
1849
+ return { content: [{ type: "text", text: `Connection initiated but status is ${connectionResult.status}. This may require OAuth — direct the user to the Browse Toolkits UI.` }] };
1850
+ }
1851
+
1852
+ case "list_integration_connections": {
1853
+ const providerId = args.provider;
1854
+ const projectId = args.project_id || null;
1855
+ const provider = getProvider(providerId);
1856
+ if (!provider) {
1857
+ return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}` }], isError: true };
1858
+ }
1859
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1860
+ if (!apiKey) {
1861
+ return { content: [{ type: "text", text: `${provider.name} API key not configured` }], isError: true };
1862
+ }
1863
+ const accounts = await provider.listConnectedAccounts(apiKey, "platform-agent");
1864
+ const result = accounts.map(a => ({
1865
+ id: a.id,
1866
+ appName: a.appName,
1867
+ appId: a.appId,
1868
+ status: a.status,
1869
+ createdAt: a.createdAt,
1870
+ }));
1871
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1872
+ }
1873
+
1874
+ case "disconnect_integration_app": {
1875
+ const providerId = args.provider;
1876
+ const projectId = args.project_id || null;
1877
+ const provider = getProvider(providerId);
1878
+ if (!provider) {
1879
+ return { content: [{ type: "text", text: `Unknown integration provider: ${providerId}` }], isError: true };
1880
+ }
1881
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1882
+ if (!apiKey) {
1883
+ return { content: [{ type: "text", text: `${provider.name} API key not configured` }], isError: true };
1884
+ }
1885
+ const success = await provider.disconnect(apiKey, args.connection_id);
1886
+ return { content: [{ type: "text", text: success ? `Disconnected successfully` : `Failed to disconnect` }], isError: !success };
1887
+ }
1888
+
1889
+ case "list_integration_configs": {
1890
+ const providerId = args.provider;
1891
+ const projectId = args.project_id || null;
1892
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1893
+ if (!apiKey) {
1894
+ return { content: [{ type: "text", text: `${providerId} API key not configured` }], isError: true };
1895
+ }
1896
+
1897
+ if (providerId === "agentdojo") {
1898
+ const servers = await listAgentDojoServers(apiKey, true);
1899
+ const result = servers.map(s => ({
1900
+ id: s.id,
1901
+ name: s.name,
1902
+ slug: s.slug,
1903
+ url: s.url,
1904
+ toolsCount: s.tools?.length || 0,
1905
+ }));
1906
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1907
+ } else if (providerId === "composio") {
1908
+ const servers = await listComposioServers(apiKey);
1909
+ const result = servers.map(s => ({
1910
+ id: s.id,
1911
+ name: s.name,
1912
+ url: s.mcpUrl,
1913
+ toolkits: s.toolkits,
1914
+ }));
1915
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1916
+ }
1917
+ return { content: [{ type: "text", text: `Config listing not supported for provider: ${providerId}` }], isError: true };
1918
+ }
1919
+
1920
+ case "create_integration_config": {
1921
+ const providerId = args.provider;
1922
+ const projectId = args.project_id || null;
1923
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1924
+ if (!apiKey) {
1925
+ return { content: [{ type: "text", text: `${providerId} API key not configured` }], isError: true };
1926
+ }
1927
+
1928
+ if (providerId === "agentdojo") {
1929
+ const server = await createAgentDojoServer(apiKey, args.name, [args.toolkit_slug]);
1930
+ if (!server) {
1931
+ return { content: [{ type: "text", text: "Failed to create MCP config on AgentDojo" }], isError: true };
1932
+ }
1933
+ return { content: [{ type: "text", text: `MCP config created on AgentDojo:\n${JSON.stringify({ id: server.id, name: server.name, slug: server.slug, url: server.url }, null, 2)}\n\nUse add_integration_config_locally to add it as a local MCP server.` }] };
1934
+ } else if (providerId === "composio") {
1935
+ // For Composio, we need the authConfigId for the toolkit
1936
+ const authConfigId = await getComposioAuthConfig(apiKey, args.toolkit_slug);
1937
+ if (!authConfigId) {
1938
+ return { content: [{ type: "text", text: `No auth config found for toolkit "${args.toolkit_slug}". Make sure the app is connected first.` }], isError: true };
1939
+ }
1940
+ const server = await createComposioServer(apiKey, args.name, [authConfigId]);
1941
+ if (!server) {
1942
+ return { content: [{ type: "text", text: "Failed to create MCP config on Composio" }], isError: true };
1943
+ }
1944
+ // Create server instance for the user
1945
+ const userId = await getComposioUserForAuth(apiKey, authConfigId);
1946
+ if (userId) {
1947
+ await createComposioInstance(apiKey, server.id, userId);
1948
+ }
1949
+ return { content: [{ type: "text", text: `MCP config created on Composio:\n${JSON.stringify({ id: server.id, name: server.name, url: server.mcpUrl }, null, 2)}\n\nUse add_integration_config_locally to add it as a local MCP server.` }] };
1950
+ }
1951
+ return { content: [{ type: "text", text: `Config creation not supported for provider: ${providerId}` }], isError: true };
1952
+ }
1953
+
1954
+ case "add_integration_config_locally": {
1955
+ const providerId = args.provider;
1956
+ const projectId = args.project_id || null;
1957
+ const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
1958
+ if (!apiKey) {
1959
+ return { content: [{ type: "text", text: `${providerId} API key not configured` }], isError: true };
1960
+ }
1961
+
1962
+ const effectiveProjectId = projectId && projectId !== "unassigned" ? projectId : null;
1963
+
1964
+ if (providerId === "agentdojo") {
1965
+ const server = await getAgentDojoServer(apiKey, args.config_id);
1966
+ if (!server) {
1967
+ return { content: [{ type: "text", text: `Config not found on AgentDojo: ${args.config_id}` }], isError: true };
1968
+ }
1969
+ // Check if already exists locally
1970
+ const existing = McpServerDB.findAllLight().find(
1971
+ s => s.source === "agentdojo" && s.project_id === effectiveProjectId && s.url?.endsWith(`/${server.slug}`)
1972
+ );
1973
+ if (existing) {
1974
+ return { content: [{ type: "text", text: `Server "${server.name}" already exists locally (ID: ${existing.id})` }] };
1975
+ }
1976
+ const localServer = McpServerDB.create({
1977
+ id: generateId(),
1978
+ name: server.name,
1979
+ type: "http",
1980
+ package: null,
1981
+ command: null,
1982
+ args: null,
1983
+ pip_module: null,
1984
+ env: {},
1985
+ url: server.url,
1986
+ headers: { "X-API-Key": apiKey },
1987
+ source: "agentdojo",
1988
+ project_id: effectiveProjectId,
1989
+ });
1990
+ return { content: [{ type: "text", text: `Server "${server.name}" added locally (ID: ${localServer.id}). Use assign_mcp_server_to_agent to give agents access.` }] };
1991
+ } else if (providerId === "composio") {
1992
+ // Fetch config details from Composio
1993
+ const res = await fetch(`https://backend.composio.dev/api/v3/mcp/${args.config_id}`, {
1994
+ headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
1995
+ });
1996
+ if (!res.ok) {
1997
+ return { content: [{ type: "text", text: `Config not found on Composio: ${args.config_id}` }], isError: true };
1998
+ }
1999
+ const data = await res.json();
2000
+ const configName = data.name || `composio-${args.config_id.slice(0, 8)}`;
2001
+ const mcpUrl = data.mcp_url;
2002
+ if (!mcpUrl) {
2003
+ return { content: [{ type: "text", text: "Config does not have an MCP URL" }], isError: true };
2004
+ }
2005
+ // Check if already exists locally
2006
+ const existing = McpServerDB.findAllLight().find(
2007
+ s => s.source === "composio" && s.url?.includes(args.config_id)
2008
+ );
2009
+ if (existing) {
2010
+ return { content: [{ type: "text", text: `Server "${configName}" already exists locally (ID: ${existing.id})` }] };
2011
+ }
2012
+ // Get user_id for URL auth
2013
+ const authConfigIds = data.auth_config_ids || [];
2014
+ let userId: string | null = null;
2015
+ if (authConfigIds.length > 0) {
2016
+ userId = await getComposioUserForAuth(apiKey, authConfigIds[0]);
2017
+ }
2018
+ const mcpUrlWithUser = userId ? `${mcpUrl}?user_id=${encodeURIComponent(userId)}` : mcpUrl;
2019
+ const localServer = McpServerDB.create({
2020
+ id: generateId(),
2021
+ name: configName,
2022
+ type: "http",
2023
+ package: null,
2024
+ command: null,
2025
+ args: null,
2026
+ pip_module: null,
2027
+ env: {},
2028
+ url: mcpUrlWithUser,
2029
+ headers: { "x-api-key": apiKey },
2030
+ source: "composio",
2031
+ project_id: effectiveProjectId,
2032
+ });
2033
+ return { content: [{ type: "text", text: `Server "${configName}" added locally (ID: ${localServer.id}). Use assign_mcp_server_to_agent to give agents access.` }] };
2034
+ }
2035
+ return { content: [{ type: "text", text: `Local add not supported for provider: ${providerId}` }], isError: true };
2036
+ }
2037
+
1316
2038
  default:
1317
2039
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
1318
2040
  }
@@ -1363,16 +2085,22 @@ export async function handlePlatformMcpRequest(req: Request): Promise<Response>
1363
2085
  instructions: `This MCP server controls the Apteva AI agent management platform.
1364
2086
 
1365
2087
  You can manage:
1366
- - AGENTS: Create, configure, start, stop, and delete AI agents. Each agent has a provider (LLM), model, system prompt, and optional features (memory, tasks, vision, MCP tools, files).
1367
- - PROJECTS: Organize agents into projects for grouping.
1368
- - MCP SERVERS: Tool integrations that give agents capabilities (web search, file access, APIs). Assign servers to agents.
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.
2088
+ - AGENTS: Create, configure, start, stop, and delete AI agents. Each agent has a provider (LLM), model, system prompt, and optional features (memory, tasks, vision, MCP tools, files). Use update_agent to assign/remove MCP servers (add_mcp_servers, remove_mcp_servers) and skills (add_skills, remove_skills).
2089
+ - TASKS: List, create, delete, and execute tasks on running agents. Tasks are scheduled work items that agents execute autonomously. Agents must have the tasks feature enabled. Tools: list_agent_tasks, create_agent_task, delete_agent_task, execute_agent_task.
2090
+ - PROJECTS: Organize agents into projects. Tools: list_projects, create_project, update_project, delete_project. Use project tools when the user is viewing All Projects. Deleting a project unassigns its agents (does not delete them).
2091
+ - MCP SERVERS: Tool integrations that give agents capabilities (web search, file access, APIs). Use update_agent with add_mcp_servers to assign servers to agents.
2092
+ - 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). Use update_agent with add_skills/remove_skills to assign/unassign skills to agents. Tools: list_skills, get_skill, create_skill, update_skill, toggle_skill, delete_skill.
1370
2093
  - PROVIDERS: View which LLM providers have API keys configured.
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.
2094
+ - 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. Tools: list_tests, create_test, run_test, run_all_tests, get_test_results, delete_test.
2095
+ - SUBSCRIPTIONS & TRIGGERS: Subscribe agents to external events (webhooks). Supports multiple providers (composio, agentdojo). Use list_trigger_providers → list_trigger_types → list_integration_connections → create_subscription. Manage with enable_subscription, disable_subscription, delete_subscription, list_subscriptions.
2096
+ - INTEGRATIONS: Connect third-party apps and create MCP servers from them. Supports agentdojo and composio providers. Use list_integration_providers → list_integration_apps → connect_integration_app (API key) → create_integration_config → add_integration_config_locally → then update_agent with add_mcp_servers to assign to an agent. For OAuth apps, direct the user to the Browse Toolkits UI.
2097
+
2098
+ CRITICAL: ALWAYS pass project_id to every tool call that accepts it. API keys and resources are scoped per project — calls without project_id will fail. The chat context tells you the current project id.
1373
2099
 
1374
- Typical workflow: list_providers → create_agent → assign MCP servers/skills → start_agent.
1375
- Subscription workflow: list_trigger_providerslist_trigger_types (pick trigger) → list_connected_accounts (pick account) → create_subscription (link trigger to agent).
2100
+ Typical workflow: list_providers → create_agent → update_agent (add_mcp_servers/add_skills) → start_agent.
2101
+ Task workflow: list_agent_taskscreate_agent_task (schedule work) → execute_agent_task (run immediately).
2102
+ Integration workflow: list_integration_providers → list_integration_apps (browse) → connect_integration_app (API key) → create_integration_config → add_integration_config_locally → update_agent (add_mcp_servers).
2103
+ Subscription workflow: list_trigger_providers → list_trigger_types (pick trigger) → list_integration_connections (pick account) → create_subscription (link trigger to agent).
1376
2104
  Test workflow: create_test (set agent, message, eval criteria) → run_test → check results.
1377
2105
  Always use list_providers first to check which providers have API keys before creating agents.`,
1378
2106
  };
@@ -1386,7 +2114,7 @@ Always use list_providers first to check which providers have API keys before cr
1386
2114
  }
1387
2115
 
1388
2116
  case "tools/list": {
1389
- result = { tools: PLATFORM_TOOLS };
2117
+ result = { tools: getPlatformTools() };
1390
2118
  break;
1391
2119
  }
1392
2120