agent-planner-mcp 1.1.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_GUIDE.md CHANGED
@@ -9,7 +9,7 @@ get_started()
9
9
  // → Returns the BDI tool surface map and recommended workflows
10
10
  ```
11
11
 
12
- ## The 24 tools
12
+ ## The 29 tools
13
13
 
14
14
  ### Beliefs (read state)
15
15
  | Tool | When |
@@ -26,7 +26,8 @@ get_started()
26
26
  |---|---|
27
27
  | `list_goals` | Goal list with health summary |
28
28
  | `update_goal` | Atomic goal change (subsumes link/unlink/achievers) |
29
- | `derive_subgoal` | Propose a sub-goal under an existing parent (top-level goals stay UI-only) |
29
+ | `create_goal` | Create a new top-level goal (no parent) agents create goals directly when asked |
30
+ | `derive_subgoal` | Create a sub-goal under an existing parent |
30
31
 
31
32
  ### Intentions — execution
32
33
  | Tool | When |
@@ -64,6 +65,17 @@ get_started()
64
65
  | `update_member_role` | Owner-only role change |
65
66
  | `remove_member` | Owner/admin removes non-owner member |
66
67
 
68
+ ### Workspaces & Blueprints (v1.1)
69
+ A Workspace is a folder under an Organization that owns goals + plans. A Blueprint is a reusable shape that forks into a workspace as a new plan.
70
+
71
+ | Tool | When |
72
+ |---|---|
73
+ | `list_workspaces` | List workspaces in an org (default and any user-created folders) |
74
+ | `create_workspace` | Create a new workspace inside an org |
75
+ | `list_blueprints` | List blueprints visible to user (owned + public/unlisted) |
76
+ | `fork_blueprint` | Instantiate a plan-scope blueprint as a new plan in a target workspace |
77
+ | `save_as_blueprint` | Snapshot a live plan as a reusable blueprint (excludes run-state) |
78
+
67
79
  ## status='active' vs status='draft' (v1.0)
68
80
 
69
81
  The single most important decision when calling a creation tool.
@@ -128,7 +140,8 @@ form_intention({goal_id: <new>, title, rationale, status: 'draft', tree: [...]})
128
140
  When in doubt between act and queue:
129
141
  - Reversible local action (status, log, learning, edit, decompose) → **act** via `update_task`, `update_node`, `extend_intention`, `add_learning`
130
142
  - External cost, public publish, strategy change, customer comm → **queue** via `queue_decision`
131
- - Whole new direction or sub-goal you weren't asked forpropose as **draft** via `form_intention` / `derive_subgoal` with `status='draft'`
143
+ - A goal a human asked you to set upjust **create** it via `create_goal` (active) no UI step, no approval gate
144
+ - A whole new direction or sub-goal you weren't asked for → propose as **draft** via `create_goal` / `form_intention` / `derive_subgoal` with `status='draft'`
132
145
 
133
146
  Never use `add_learning(entry_type='decision')` to fake a decision queue. `queue_decision` is the real tool.
134
147
 
package/README.md CHANGED
@@ -255,7 +255,8 @@ Add the same JSON config to your Cline MCP settings in VS Code.
255
255
  ### Desires (goals)
256
256
  - `list_goals` — goals with health rollup
257
257
  - `update_goal` — atomic goal update (subsumes link/unlink/achievers)
258
- - `derive_subgoal` *(v1.0)* propose a sub-goal under an existing parent
258
+ - `create_goal` — create a new top-level goal (no parent)
259
+ - `derive_subgoal` *(v1.0)* — create a sub-goal under an existing parent
259
260
 
260
261
  ### Intentions — execution
261
262
  - `claim_next_task` — pick + claim + load context (one call)
package/SKILL.md CHANGED
@@ -23,7 +23,7 @@ You have access to the AgentPlanner MCP tools. AgentPlanner is a collaborative p
23
23
  > - **Cursor / VS Code:** Add `npx agent-planner-mcp` to your MCP config with env vars `API_URL` and `USER_API_TOKEN`
24
24
  > - **ChatGPT:** HTTP endpoint at `https://agentplanner.io/mcp`
25
25
 
26
- ## The 24 tools, organized by intent
26
+ ## The 29 tools, organized by intent
27
27
 
28
28
  AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desires (goal management), Intentions (committed actions). Each tool answers one whole agentic question and returns an `as_of` ISO 8601 timestamp. v1.0.0 completes the mutation surface so humans can steer entirely through agent conversation — no UI required for normal operations.
29
29
 
@@ -40,7 +40,8 @@ AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desi
40
40
 
41
41
  - `list_goals` — goals with health rollup (`{ on_track, at_risk, stale, total }`)
42
42
  - `update_goal` — atomic goal update; subsumes link/unlink + achiever changes
43
- - `derive_subgoal` *(v1.0)* propose a sub-goal under an existing parent. Top-level goal creation stays UI-only.
43
+ - `create_goal` — create a new top-level goal (no parent). Agents create goals directly when asked — no UI step. Defaults to `status='active'`.
44
+ - `derive_subgoal` *(v1.0)* — create a sub-goal under an existing parent (use `create_goal` for top-level).
44
45
 
45
46
  ### Intentions — what am I committing to?
46
47
 
@@ -72,6 +73,16 @@ AgentPlanner exposes a **BDI-aligned** surface — Beliefs (state queries), Desi
72
73
  - `update_member_role` — owner-only role change within an org
73
74
  - `remove_member` — owner/admin can remove non-owner members
74
75
 
76
+ **Workspaces and Blueprints (v1.1):**
77
+
78
+ A Workspace is a folder under an Organization that owns goals + plans — a grouping primitive so a single org isn't a flat soup of unrelated work. A Blueprint is a dehydrated, reusable shape (scope `plan` or `workspace`); forking instantiates it as a new plan inside a target workspace. v1 supports plan-scope only.
79
+
80
+ - `list_workspaces` — list workspaces in an organization
81
+ - `create_workspace` — create a new folder under an org (auto-slug)
82
+ - `list_blueprints` — list blueprints visible to user (owned + public/unlisted), filterable by scope
83
+ - `fork_blueprint` — instantiate a plan-scope blueprint as a new plan in a target workspace
84
+ - `save_as_blueprint` — snapshot a live plan as a reusable blueprint. Captures structure, agent_instructions, and dependencies; excludes statuses, claims, knowledge episodes, logs, decisions, and agent assignments
85
+
75
86
  ### Utility
76
87
 
77
88
  - `get_started` — dynamic reference; call this if you're new to AgentPlanner
@@ -250,12 +261,14 @@ When a user expresses intent — "I want to launch a feature", "we need better t
250
261
  2. list_goals to check if a similar goal already exists
251
262
  3. Use update_goal({ add_linked_plans, add_achievers }) to wire it up
252
263
 
253
- Goal types:
254
- - desire — aspirational, no firm deadline
255
- - intention — promoted from desire when execution begins
264
+ Goal commitment (`committed` boolean):
265
+ - committed: false — aspirational, no firm commitment to execute yet
266
+ - committed: true — promoted to active execution
256
267
  ```
257
268
 
258
- Promote desire intention via `update_goal({ promote_to_intention: true })`.
269
+ Commit a goal via `update_goal({ changes: { committed: true } })`. Coherence
270
+ status on tasks reads as plain language: `ok` / `outdated` / `contradicted` /
271
+ `unchecked` (with a `coherence_message`).
259
272
 
260
273
  ## Decision queueing
261
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for AgentPlanner — AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/api-client.js CHANGED
@@ -550,6 +550,7 @@ const goals = {
550
550
  const params = new URLSearchParams();
551
551
  if (filters.organization_id) params.append('organization_id', filters.organization_id);
552
552
  if (filters.status) params.append('status', filters.status);
553
+ if (filters.workspaceId) params.append('workspace_id', filters.workspaceId);
553
554
  const response = await apiClient.get(`/goals?${params.toString()}`);
554
555
  return response.data.goals || response.data;
555
556
  },
@@ -744,6 +745,40 @@ const graphiti = {
744
745
  }
745
746
  };
746
747
 
748
+ // ─── Workspaces ───────────────────────────────────────────────
749
+ const workspaces = {
750
+ list: async ({ organizationId, includeArchived = false } = {}) => {
751
+ const params = new URLSearchParams();
752
+ if (organizationId) params.append('organization_id', organizationId);
753
+ if (includeArchived) params.append('include_archived', 'true');
754
+ return (await apiClient.get(`/workspaces?${params.toString()}`)).data;
755
+ },
756
+ get: async (workspaceId) => (await apiClient.get(`/workspaces/${workspaceId}`)).data,
757
+ create: async (data) => (await apiClient.post('/workspaces', data)).data,
758
+ update: async (workspaceId, data) => (await apiClient.patch(`/workspaces/${workspaceId}`, data)).data,
759
+ archive: async (workspaceId) => (await apiClient.post(`/workspaces/${workspaceId}/archive`)).data,
760
+ restore: async (workspaceId) => (await apiClient.post(`/workspaces/${workspaceId}/restore`)).data,
761
+ delete: async (workspaceId) => (await apiClient.delete(`/workspaces/${workspaceId}`)).data,
762
+ };
763
+
764
+ // ─── Blueprints ───────────────────────────────────────────────
765
+ const blueprints = {
766
+ list: async ({ scope, visibility, ownerOnly = false } = {}) => {
767
+ const params = new URLSearchParams();
768
+ if (scope) params.append('scope', scope);
769
+ if (visibility) params.append('visibility', visibility);
770
+ if (ownerOnly) params.append('owner_only', 'true');
771
+ const qs = params.toString();
772
+ return (await apiClient.get(`/blueprints${qs ? `?${qs}` : ''}`)).data;
773
+ },
774
+ get: async (blueprintId) => (await apiClient.get(`/blueprints/${blueprintId}`)).data,
775
+ create: async (data) => (await apiClient.post('/blueprints', data)).data,
776
+ update: async (blueprintId, data) => (await apiClient.patch(`/blueprints/${blueprintId}`, data)).data,
777
+ delete: async (blueprintId) => (await apiClient.delete(`/blueprints/${blueprintId}`)).data,
778
+ fork: async (blueprintId, data) => (await apiClient.post(`/blueprints/${blueprintId}/fork`, data)).data,
779
+ saveFromPlan: async (planId, data = {}) => (await apiClient.post(`/blueprints/from_plan/${planId}`, data)).data,
780
+ };
781
+
747
782
  // ─── Users (my-tasks queue) ────────────────────────────────────
748
783
  const users = {
749
784
  getMyTasks: async (options = {}) => {
@@ -764,6 +799,22 @@ const agentLoop = {
764
799
  createIntention: async (data = {}) => (await apiClient.post('/agent/intentions', data)).data,
765
800
  };
766
801
 
802
+ // ─── v1 public API facades ────────────────────────────────────
803
+ // Server-side compositions shipped with the API v1 consolidation. Each
804
+ // replaces a client-side fan-out (goal_state: 5 calls → 1, recall_knowledge:
805
+ // up to 4 → 1, update_task: 4 → 1, share_plan: N → 1). Tool handlers fall
806
+ // back to the legacy fan-out when the backend predates /v1 — see
807
+ // isV1Unavailable() in tools/bdi/_shared.js.
808
+ const v1 = {
809
+ goalState: async (goalId) => (await apiClient.get(`/v1/goals/${goalId}/state`)).data,
810
+ planAnalysis: async (planId) => (await apiClient.get(`/v1/plans/${planId}/analysis`)).data,
811
+ knowledgeSearch: async (body = {}) => (await apiClient.post('/v1/knowledge/search', body)).data,
812
+ updateTask: async (nodeId, body = {}) => (await apiClient.post(`/v1/tasks/${nodeId}/update`, body)).data,
813
+ sharePlan: async (planId, body = {}) => (await apiClient.post(`/v1/plans/${planId}/share`, body)).data,
814
+ briefing: async (params = {}) => (await apiClient.get('/v1/briefing', { params })).data,
815
+ claimNext: async (body = {}) => (await apiClient.post('/v1/tasks/claim-next', body)).data,
816
+ };
817
+
767
818
  // ─── Dependencies (cross-plan & external) ─────────────────────
768
819
  const dependencies = {
769
820
  /**
@@ -918,6 +969,7 @@ function createApiClient(token, options = {}) {
918
969
  const params = new URLSearchParams();
919
970
  if (filters.organization_id) params.append('organization_id', filters.organization_id);
920
971
  if (filters.status) params.append('status', filters.status);
972
+ if (filters.workspaceId) params.append('workspace_id', filters.workspaceId);
921
973
  const r = await client.get(`/goals?${params.toString()}`);
922
974
  return r.data.goals || r.data;
923
975
  },
@@ -973,6 +1025,15 @@ function createApiClient(token, options = {}) {
973
1025
  listCrossPlan: async (planIds) => (await client.get('/dependencies/cross-plan', { params: { plan_ids: planIds.join(',') } })).data,
974
1026
  createExternal: async (data) => (await client.post('/dependencies/external', data)).data,
975
1027
  },
1028
+ v1: {
1029
+ goalState: async (goalId) => (await client.get(`/v1/goals/${goalId}/state`)).data,
1030
+ planAnalysis: async (planId) => (await client.get(`/v1/plans/${planId}/analysis`)).data,
1031
+ knowledgeSearch: async (body = {}) => (await client.post('/v1/knowledge/search', body)).data,
1032
+ updateTask: async (nodeId, body = {}) => (await client.post(`/v1/tasks/${nodeId}/update`, body)).data,
1033
+ sharePlan: async (planId, body = {}) => (await client.post(`/v1/plans/${planId}/share`, body)).data,
1034
+ briefing: async (params = {}) => (await client.get('/v1/briefing', { params })).data,
1035
+ claimNext: async (body = {}) => (await client.post('/v1/tasks/claim-next', body)).data,
1036
+ },
976
1037
  users: {
977
1038
  getMyTasks: async (options = {}) => {
978
1039
  const params = new URLSearchParams();
@@ -988,6 +1049,36 @@ function createApiClient(token, options = {}) {
988
1049
  blockWorkSession: async (sessionId, data = {}) => (await client.post(`/agent/work-sessions/${sessionId}/block`, data)).data,
989
1050
  createIntention: async (data = {}) => (await client.post('/agent/intentions', data)).data,
990
1051
  },
1052
+ workspaces: {
1053
+ list: async ({ organizationId, includeArchived = false } = {}) => {
1054
+ const params = new URLSearchParams();
1055
+ if (organizationId) params.append('organization_id', organizationId);
1056
+ if (includeArchived) params.append('include_archived', 'true');
1057
+ return (await client.get(`/workspaces?${params.toString()}`)).data;
1058
+ },
1059
+ get: async (workspaceId) => (await client.get(`/workspaces/${workspaceId}`)).data,
1060
+ create: async (data) => (await client.post('/workspaces', data)).data,
1061
+ update: async (workspaceId, data) => (await client.patch(`/workspaces/${workspaceId}`, data)).data,
1062
+ archive: async (workspaceId) => (await client.post(`/workspaces/${workspaceId}/archive`)).data,
1063
+ restore: async (workspaceId) => (await client.post(`/workspaces/${workspaceId}/restore`)).data,
1064
+ delete: async (workspaceId) => (await client.delete(`/workspaces/${workspaceId}`)).data,
1065
+ },
1066
+ blueprints: {
1067
+ list: async ({ scope, visibility, ownerOnly = false } = {}) => {
1068
+ const params = new URLSearchParams();
1069
+ if (scope) params.append('scope', scope);
1070
+ if (visibility) params.append('visibility', visibility);
1071
+ if (ownerOnly) params.append('owner_only', 'true');
1072
+ const qs = params.toString();
1073
+ return (await client.get(`/blueprints${qs ? `?${qs}` : ''}`)).data;
1074
+ },
1075
+ get: async (blueprintId) => (await client.get(`/blueprints/${blueprintId}`)).data,
1076
+ create: async (data) => (await client.post('/blueprints', data)).data,
1077
+ update: async (blueprintId, data) => (await client.patch(`/blueprints/${blueprintId}`, data)).data,
1078
+ delete: async (blueprintId) => (await client.delete(`/blueprints/${blueprintId}`)).data,
1079
+ fork: async (blueprintId, data) => (await client.post(`/blueprints/${blueprintId}/fork`, data)).data,
1080
+ saveFromPlan: async (planId, data = {}) => (await client.post(`/blueprints/from_plan/${planId}`, data)).data,
1081
+ },
991
1082
  axiosInstance: client,
992
1083
  };
993
1084
  }
@@ -1013,12 +1104,15 @@ module.exports = {
1013
1104
  tokens,
1014
1105
  organizations,
1015
1106
  goals,
1107
+ workspaces,
1108
+ blueprints,
1016
1109
  context,
1017
1110
  graphiti,
1018
1111
  dependencies,
1019
1112
  coherence,
1020
1113
  users,
1021
1114
  agentLoop,
1115
+ v1,
1022
1116
  axiosInstance, // Export for direct API calls
1023
1117
  createApiClient // Factory for per-session clients (HTTP mode)
1024
1118
  };
@@ -195,13 +195,15 @@ function renderPlanHealth(plan) {
195
195
  function renderCoherenceWarning(task) {
196
196
  if (!task || !task.coherence_status) return [];
197
197
  const status = task.coherence_status;
198
- if (status === 'clean' || status === 'unchecked') return [];
198
+ if (status === 'ok' || status === 'coherent' || status === 'clean' || status === 'unchecked') return [];
199
199
 
200
200
  const out = ['## Coherence warning', ''];
201
201
  out.push(`- Status: ${status}`);
202
- if (status === 'contradiction_detected') {
202
+ // Accept both the public vocabulary and the legacy internal values so the
203
+ // CLI keeps working against older backends.
204
+ if (status === 'contradicted' || status === 'contradiction_detected') {
203
205
  out.push('- Supporting knowledge contains contradictions. Run `check_contradictions` (MCP) and re-verify before acting.');
204
- } else if (status === 'stale_beliefs') {
206
+ } else if (status === 'outdated' || status === 'stale_beliefs') {
205
207
  out.push('- Knowledge backing this task may be outdated. Run `recall_knowledge` (MCP) to refresh before deciding.');
206
208
  }
207
209
  out.push('');
@@ -26,4 +26,16 @@ function safeArray(value) {
26
26
  return Array.isArray(value) ? value : [];
27
27
  }
28
28
 
29
- module.exports = { asOf, formatResponse, errorResponse, safeArray };
29
+ /**
30
+ * True when an error means the backend has no /v1 surface (pre-consolidation
31
+ * self-hosted API). Express returns a default 404 with no structured body for
32
+ * unmatched routes, whereas v1 handlers always return JSON with an `error`
33
+ * field — so a bare 404 means "route missing", not "resource missing".
34
+ */
35
+ function isV1Unavailable(err) {
36
+ if (err.response?.status !== 404) return false;
37
+ const body = err.response.data;
38
+ return !(body && typeof body === 'object' && body.error);
39
+ }
40
+
41
+ module.exports = { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable };
@@ -5,7 +5,7 @@
5
5
  * plan_analysis. Each answers one whole agentic question and returns `as_of`.
6
6
  */
7
7
 
8
- const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
8
+ const { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable } = require('./_shared');
9
9
 
10
10
  // ─────────────────────────────────────────────────────────────────────────
11
11
  // briefing — bundled mission control state. Replaces 4 round trips.
@@ -30,15 +30,24 @@ const briefingDefinition = {
30
30
  };
31
31
 
32
32
  async function briefingHandler(args, apiClient) {
33
+ const briefingParams = {
34
+ scope: args.scope,
35
+ goal_id: args.goal_id,
36
+ plan_id: args.plan_id,
37
+ recent_window_hours: args.recent_window_hours,
38
+ };
39
+
40
+ // v1 public path first; same server-side handler as /agent/briefing.
41
+ if (apiClient.v1) {
42
+ try {
43
+ return formatResponse(await apiClient.v1.briefing(briefingParams));
44
+ } catch {
45
+ // Fall through to the internal facade path, then the legacy fan-out.
46
+ }
47
+ }
48
+
33
49
  try {
34
- const response = await apiClient.axiosInstance.get('/agent/briefing', {
35
- params: {
36
- scope: args.scope,
37
- goal_id: args.goal_id,
38
- plan_id: args.plan_id,
39
- recent_window_hours: args.recent_window_hours,
40
- },
41
- });
50
+ const response = await apiClient.axiosInstance.get('/agent/briefing', { params: briefingParams });
42
51
  return formatResponse(response.data);
43
52
  } catch {
44
53
  // Fall back to the pre-facade fan-out for self-hosted older APIs.
@@ -232,6 +241,22 @@ const goalStateDefinition = {
232
241
 
233
242
  async function goalStateHandler(args, apiClient) {
234
243
  const { goal_id } = args;
244
+
245
+ // v1 facade: one server-side call replaces the 5-endpoint fan-out below.
246
+ // Same response shape — the server composition was ported from this handler.
247
+ if (apiClient.v1) {
248
+ try {
249
+ return formatResponse(await apiClient.v1.goalState(goal_id));
250
+ } catch (err) {
251
+ if (!isV1Unavailable(err)) {
252
+ const status = err.response?.status;
253
+ if (status === 404) return errorResponse('not_found', `Goal ${goal_id} not found`);
254
+ if (status === 403) return errorResponse('forbidden', 'Access denied to this goal');
255
+ // 5xx and friends: fall through to the legacy fan-out best-effort path.
256
+ }
257
+ }
258
+ }
259
+
235
260
  const [goalRes, qualityRes, progressRes, gapsRes, pathRes] = await Promise.allSettled([
236
261
  apiClient.goals.get(goal_id),
237
262
  apiClient.goals.getQuality(goal_id),
@@ -283,7 +308,7 @@ async function goalStateHandler(args, apiClient) {
283
308
  as_of: asOf(),
284
309
  goal: {
285
310
  id: goal.id, title: goal.title, description: goal.description,
286
- type: goal.type, goal_type: goal.goalType || goal.goal_type,
311
+ type: goal.type, committed: Boolean(goal.committed ?? goal.promotedAt ?? goal.promoted_at),
287
312
  status: goal.status, priority: goal.priority,
288
313
  owner_id: goal.ownerId || goal.owner_id, success_criteria: goal.successCriteria || goal.success_criteria,
289
314
  promoted_at: goal.promotedAt || goal.promoted_at,
@@ -330,6 +355,22 @@ const recallKnowledgeDefinition = {
330
355
 
331
356
  async function recallKnowledgeHandler(args, apiClient) {
332
357
  const { query, scope = {}, since, entry_type = 'all', result_kind = 'all', max_results = 10, include_contradictions = false } = args;
358
+
359
+ // v1 facade: one server-side call replaces the 4-endpoint fan-out below.
360
+ if (apiClient.v1) {
361
+ try {
362
+ const data = await apiClient.v1.knowledgeSearch({
363
+ query, since, entry_type, result_kind, max_results, include_contradictions, ...scope,
364
+ });
365
+ return formatResponse(data);
366
+ } catch (err) {
367
+ if (!isV1Unavailable(err)) {
368
+ // v1 exists but errored — keep going with the legacy fan-out, which
369
+ // already treats every sub-call as best-effort.
370
+ }
371
+ }
372
+ }
373
+
333
374
  const wantFacts = result_kind === 'all' || result_kind === 'facts';
334
375
  const wantEntities = result_kind === 'all' || result_kind === 'entities';
335
376
  const wantEpisodes = result_kind === 'all' || result_kind === 'episodes';
@@ -399,6 +440,7 @@ const listPlansDefinition = {
399
440
  status: { type: 'array', items: { type: 'string' } },
400
441
  visibility: { type: 'array', items: { type: 'string', enum: ['private', 'unlisted', 'public'] } },
401
442
  query: { type: 'string', description: 'Substring match on title (case-insensitive)' },
443
+ workspace_id: { type: 'string', description: 'Scope to plans inside a single workspace' },
402
444
  limit: { type: 'integer', default: 50 },
403
445
  },
404
446
  },
@@ -414,6 +456,9 @@ async function listPlansHandler(args, apiClient) {
414
456
 
415
457
  if (filter.status?.length) plans = plans.filter((p) => filter.status.includes(p.status));
416
458
  if (filter.visibility?.length) plans = plans.filter((p) => filter.visibility.includes(p.visibility));
459
+ if (filter.workspace_id) {
460
+ plans = plans.filter((p) => (p.workspace_id || p.workspaceId) === filter.workspace_id);
461
+ }
417
462
  if (filter.query) {
418
463
  const q = filter.query.toLowerCase();
419
464
  plans = plans.filter((p) => (p.title || '').toLowerCase().includes(q));
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * BDI desires — goal management.
3
3
  *
4
- * 3 tools: list_goals (with health rollup), update_goal (atomic, subsumes
5
- * link/unlink and achiever changes), derive_subgoal (propose a sub-goal
6
- * under an existing parent; mandatory parent — top-level goals stay UI-only).
4
+ * 4 tools: list_goals (with health rollup), update_goal (atomic, subsumes
5
+ * link/unlink and achiever changes), create_goal (new top-level goal), and
6
+ * derive_subgoal (a sub-goal under an existing parent). Agents create goals
7
+ * directly — no UI round-trip and no forced approval gate.
7
8
  */
8
9
 
9
10
  const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
@@ -21,6 +22,7 @@ const listGoalsDefinition = {
21
22
  properties: {
22
23
  health: { type: 'array', items: { type: 'string', enum: ['on_track', 'at_risk', 'stale'] } },
23
24
  status: { type: 'array', items: { type: 'string' } },
25
+ workspace_id: { type: 'string', description: 'Scope to goals inside a single workspace' },
24
26
  include_inactive: { type: 'boolean', default: false },
25
27
  },
26
28
  },
@@ -32,7 +34,7 @@ async function listGoalsHandler(args, apiClient) {
32
34
  const filter = args.filter || {};
33
35
  try {
34
36
  const [listRes, dashboardRes] = await Promise.allSettled([
35
- apiClient.goals.list({ status: filter.include_inactive ? undefined : 'active' }),
37
+ apiClient.goals.list({ status: filter.include_inactive ? undefined : 'active', workspaceId: filter.workspace_id }),
36
38
  apiClient.goals.getDashboard(),
37
39
  ]);
38
40
 
@@ -88,9 +90,10 @@ const updateGoalDefinition = {
88
90
  description: { type: 'string' },
89
91
  priority: { type: 'integer' },
90
92
  status: { type: 'string' },
91
- goal_type: { type: 'string', enum: ['desire', 'intention'] },
93
+ // Commitment: true once the goal is promoted to active execution
94
+ // (replaces the old desire/intention goal_type vocabulary).
95
+ committed: { type: 'boolean' },
92
96
  success_criteria: {},
93
- promote_to_intention: { type: 'boolean' },
94
97
  add_linked_plans: { type: 'array', items: { type: 'string' } },
95
98
  remove_linked_plans: { type: 'array', items: { type: 'string' } },
96
99
  add_achievers: { type: 'array', items: { type: 'string' } },
@@ -112,8 +115,12 @@ async function updateGoalHandler(args, apiClient) {
112
115
  for (const k of ['title', 'description', 'priority', 'status', 'success_criteria']) {
113
116
  if (changes[k] !== undefined) directFields[k] = changes[k];
114
117
  }
115
- if (changes.goal_type) directFields.goalType = changes.goal_type;
116
- if (changes.promote_to_intention) directFields.goalType = 'intention';
118
+ // Map the public `committed` boolean onto the backend's commitment write
119
+ // (the API still accepts the legacy goalType field and translates it to
120
+ // promoted_at). committed:true ⇒ promoted, false ⇒ aspirational.
121
+ if (changes.committed !== undefined) {
122
+ directFields.goalType = changes.committed ? 'intention' : 'desire';
123
+ }
117
124
 
118
125
  if (Object.keys(directFields).length) {
119
126
  try {
@@ -156,8 +163,8 @@ async function updateGoalHandler(args, apiClient) {
156
163
  }
157
164
 
158
165
  // ─────────────────────────────────────────────────────────────────────────
159
- // derive_subgoal — propose a sub-goal under an existing parent.
160
- // Top-level goals stay UI-only (strategic direction is human-set).
166
+ // derive_subgoal — create a sub-goal under an existing parent. For a new
167
+ // top-level goal use create_goal.
161
168
  // ─────────────────────────────────────────────────────────────────────────
162
169
 
163
170
  const VALID_GOAL_TYPES = ['outcome', 'constraint', 'metric', 'principle'];
@@ -166,11 +173,10 @@ const VALID_STATUSES = ['draft', 'active', 'achieved', 'paused', 'abandoned', 'a
166
173
  const deriveSubgoalDefinition = {
167
174
  name: 'derive_subgoal',
168
175
  description:
169
- "Propose a sub-goal under an existing parent goal. parent_goal_id is " +
170
- "mandatory agents cannot create top-level goals (strategic direction is " +
171
- "human-set). Defaults to status='active' for human-directed creation; pass " +
172
- "status='draft' for autonomous loops so a human can review before promotion. " +
173
- "Drafts surface in the dashboard pending queue.",
176
+ "Create a sub-goal under an existing parent goal (parent_goal_id required). " +
177
+ "For a new top-level goal, use create_goal instead. Defaults to " +
178
+ "status='active'; pass status='draft' for autonomous loops so a human can " +
179
+ "review before promotion. Drafts surface in the dashboard pending queue.",
174
180
  inputSchema: {
175
181
  type: 'object',
176
182
  properties: {
@@ -254,11 +260,85 @@ async function deriveSubgoalHandler(args, apiClient) {
254
260
  });
255
261
  }
256
262
 
263
+ // ─────────────────────────────────────────────────────────────────────────
264
+ // create_goal — create a top-level goal directly (no parent).
265
+ // Agents create goals when a human asks them to; there is no UI round-trip and
266
+ // no forced approval gate. Defaults to status='active'.
267
+ // ─────────────────────────────────────────────────────────────────────────
268
+
269
+ const createGoalDefinition = {
270
+ name: 'create_goal',
271
+ description:
272
+ "Create a new top-level goal (no parent). Use this when a human asks you to " +
273
+ "set up a goal — agents create goals directly, no UI step required. For a " +
274
+ "goal that contributes to an existing one, use derive_subgoal instead. " +
275
+ "Defaults to status='active' (live immediately); pass status='draft' only " +
276
+ "if you want it to sit in the pending queue for review. Lands in the user's " +
277
+ "active organization's default workspace unless workspace_id is given.",
278
+ inputSchema: {
279
+ type: 'object',
280
+ properties: {
281
+ title: { type: 'string', description: "The goal statement." },
282
+ description: { type: 'string', description: "What the goal means / context." },
283
+ type: {
284
+ type: 'string',
285
+ enum: VALID_GOAL_TYPES,
286
+ default: 'outcome',
287
+ description: "outcome (end state), metric (quantitative target), constraint (must-not-violate), principle (durable invariant).",
288
+ },
289
+ status: {
290
+ type: 'string',
291
+ enum: VALID_STATUSES,
292
+ default: 'active',
293
+ description: "Default 'active' (live). Pass 'draft' to propose without activating.",
294
+ },
295
+ success_criteria: {
296
+ type: 'array',
297
+ items: { type: 'string' },
298
+ description: "Concrete, observable conditions that mark this goal achieved.",
299
+ },
300
+ priority: { type: 'integer', minimum: 0, maximum: 10, default: 0 },
301
+ workspace_id: { type: 'string', description: "Optional. Target workspace; defaults to the active org's default workspace." },
302
+ },
303
+ required: ['title'],
304
+ },
305
+ };
306
+
307
+ async function createGoalHandler(args, apiClient) {
308
+ const { title, description, type = 'outcome', status = 'active', success_criteria, priority, workspace_id } = args;
309
+
310
+ const payload = { title, type, status };
311
+ if (description) payload.description = description;
312
+ if (success_criteria) payload.successCriteria = { criteria: success_criteria };
313
+ if (typeof priority === 'number') payload.priority = priority;
314
+ if (workspace_id) payload.workspaceId = workspace_id;
315
+
316
+ let goal;
317
+ try {
318
+ goal = await apiClient.goals.create(payload);
319
+ } catch (err) {
320
+ const upstream = err.response?.data?.error || err.message;
321
+ return errorResponse('create_failed', `Failed to create goal: ${upstream}`);
322
+ }
323
+
324
+ return formatResponse({
325
+ as_of: asOf(),
326
+ goal_id: goal.id,
327
+ title: goal.title,
328
+ status: goal.status,
329
+ is_draft: goal.status === 'draft',
330
+ next_step: goal.status === 'draft'
331
+ ? "Goal created as draft. Promote via update_goal({status: 'active'}) once ready."
332
+ : "Goal is active. Add sub-goals with derive_subgoal, or link plans via update_goal({add_linked_plans: [...]}).",
333
+ });
334
+ }
335
+
257
336
  module.exports = {
258
- definitions: [listGoalsDefinition, updateGoalDefinition, deriveSubgoalDefinition],
337
+ definitions: [listGoalsDefinition, updateGoalDefinition, createGoalDefinition, deriveSubgoalDefinition],
259
338
  handlers: {
260
339
  list_goals: listGoalsHandler,
261
340
  update_goal: updateGoalHandler,
341
+ create_goal: createGoalHandler,
262
342
  derive_subgoal: deriveSubgoalHandler,
263
343
  },
264
344
  };
@@ -12,12 +12,14 @@ const beliefs = require('./beliefs');
12
12
  const desires = require('./desires');
13
13
  const intentions = require('./intentions');
14
14
  const utility = require('./utility');
15
+ const workspaces = require('./workspaces');
15
16
 
16
17
  const definitions = [
17
18
  ...beliefs.definitions,
18
19
  ...desires.definitions,
19
20
  ...intentions.definitions,
20
21
  ...utility.definitions,
22
+ ...workspaces.definitions,
21
23
  ];
22
24
 
23
25
  const handlers = {
@@ -25,6 +27,7 @@ const handlers = {
25
27
  ...desires.handlers,
26
28
  ...intentions.handlers,
27
29
  ...utility.handlers,
30
+ ...workspaces.handlers,
28
31
  };
29
32
 
30
33
  const names = new Set(definitions.map((t) => t.name));
@@ -13,7 +13,7 @@
13
13
  * See ../../../docs/MCP_v1.0_FULL_SURFACE.md for design rationale.
14
14
  */
15
15
 
16
- const { asOf, formatResponse, errorResponse } = require('./_shared');
16
+ const { asOf, formatResponse, errorResponse, isV1Unavailable } = require('./_shared');
17
17
 
18
18
  // ─────────────────────────────────────────────────────────────────────────
19
19
  // queue_decision — real decision queue. Replaces add_learning workaround.
@@ -312,6 +312,28 @@ async function updateTaskHandler(args, apiClient) {
312
312
  }
313
313
  }
314
314
 
315
+ // v1 facade: one atomic server-side call (status + log + claim release +
316
+ // learning) replaces the 4-endpoint fan-out below. Same response shape.
317
+ if (apiClient.v1) {
318
+ try {
319
+ const data = await apiClient.v1.updateTask(task_id, {
320
+ status,
321
+ log_message,
322
+ log_type: args.log_type,
323
+ release_claim,
324
+ add_learning,
325
+ });
326
+ return formatResponse(data);
327
+ } catch (err) {
328
+ if (!isV1Unavailable(err)) {
329
+ const s = err.response?.status;
330
+ if (s === 404) return errorResponse('not_found', `Task ${task_id} not found`);
331
+ if (s === 403) return errorResponse('forbidden', 'Access denied to this plan');
332
+ // 5xx: fall through to the legacy fan-out.
333
+ }
334
+ }
335
+ }
336
+
315
337
  // Resolve plan_id from task if not provided.
316
338
  if (!planId) {
317
339
  try {
@@ -345,7 +367,7 @@ async function updateTaskHandler(args, apiClient) {
345
367
  if (log_message) {
346
368
  const logType = args.log_type || STATUS_TO_LOG_TYPE[status] || 'progress';
347
369
  try {
348
- const log = await apiClient.logs.addLog(planId, task_id, {
370
+ const log = await apiClient.logs.addLogEntry(planId, task_id, {
349
371
  content: log_message,
350
372
  log_type: logType,
351
373
  });
@@ -427,16 +449,27 @@ async function claimNextTaskHandler(args, apiClient) {
427
449
  const { scope = {}, ttl_minutes = 30, fresh = false, context_depth = 2, dry_run = false } = args;
428
450
  const { plan_id, goal_id } = scope;
429
451
 
452
+ const sessionBody = {
453
+ plan_id,
454
+ goal_id,
455
+ ttl_minutes,
456
+ fresh,
457
+ dry_run,
458
+ depth: context_depth,
459
+ agent_id: 'mcp-agent',
460
+ };
461
+
462
+ // v1 public path first; same server-side handler as /agent/work-sessions.
463
+ if (apiClient.v1) {
464
+ try {
465
+ return formatResponse(await apiClient.v1.claimNext(sessionBody));
466
+ } catch {
467
+ // Fall through to the internal facade path, then the legacy fan-out.
468
+ }
469
+ }
470
+
430
471
  try {
431
- const response = await apiClient.axiosInstance.post('/agent/work-sessions', {
432
- plan_id,
433
- goal_id,
434
- ttl_minutes,
435
- fresh,
436
- dry_run,
437
- depth: context_depth,
438
- agent_id: 'mcp-agent',
439
- });
472
+ const response = await apiClient.axiosInstance.post('/agent/work-sessions', sessionBody);
440
473
  return formatResponse(response.data);
441
474
  } catch {
442
475
  // Fall back to the pre-facade fan-out for self-hosted older APIs.
@@ -584,7 +617,7 @@ async function releaseTaskHandler(args, apiClient) {
584
617
  let logId = null;
585
618
  if (message) {
586
619
  try {
587
- const log = await apiClient.logs.addLog(planId, task_id, { content: message, log_type: 'progress' });
620
+ const log = await apiClient.logs.addLogEntry(planId, task_id, { content: message, log_type: 'progress' });
588
621
  logId = log?.id || log?.log?.id;
589
622
  } catch {}
590
623
  }
@@ -1024,7 +1057,10 @@ async function proposeResearchChainHandler(args, apiClient) {
1024
1057
  // Cycle detection happens server-side; we surface the 409 cleanly.
1025
1058
  // ─────────────────────────────────────────────────────────────────────────
1026
1059
 
1027
- const VALID_RELATIONS = ['blocks', 'requires', 'relates_to'];
1060
+ // Canonical node→node dependency vocabulary (backend ring-2 consolidation).
1061
+ // `requires` is still accepted by the API (mapped to `blocks`) but no longer
1062
+ // offered here.
1063
+ const VALID_RELATIONS = ['blocks', 'relates_to'];
1028
1064
 
1029
1065
  const linkIntentionsDefinition = {
1030
1066
  name: 'link_intentions',
@@ -1480,6 +1516,28 @@ const sharePlanDefinition = {
1480
1516
 
1481
1517
  async function sharePlanHandler(args, apiClient) {
1482
1518
  const { plan_id, visibility, add_collaborators = [], remove_collaborators = [] } = args;
1519
+
1520
+ // v1 facade: one atomic server-side call replaces the per-collaborator
1521
+ // fan-out below. Same response shape (applied_changes + failures).
1522
+ if (apiClient.v1) {
1523
+ try {
1524
+ const data = await apiClient.v1.sharePlan(plan_id, {
1525
+ visibility,
1526
+ add_collaborators,
1527
+ remove_collaborators,
1528
+ });
1529
+ return formatResponse(data);
1530
+ } catch (err) {
1531
+ if (!isV1Unavailable(err)) {
1532
+ const s = err.response?.status;
1533
+ if (s === 400 || s === 403 || s === 404) {
1534
+ return errorResponse('invalid_arg', err.response?.data?.error || err.message);
1535
+ }
1536
+ // 5xx: fall through to the legacy fan-out.
1537
+ }
1538
+ }
1539
+ }
1540
+
1483
1541
  const applied = [];
1484
1542
  const failures = [];
1485
1543
 
@@ -29,6 +29,7 @@ async function getStartedHandler(args) {
29
29
  beliefs: ['briefing', 'list_plans', 'task_context', 'goal_state', 'recall_knowledge', 'search', 'plan_analysis'],
30
30
  desires: ['list_goals', 'update_goal'],
31
31
  intentions: ['claim_next_task', 'update_task', 'release_task', 'queue_decision', 'resolve_decision', 'add_learning'],
32
+ workspaces: ['list_workspaces', 'create_workspace', 'list_blueprints', 'fork_blueprint', 'save_as_blueprint'],
32
33
  },
33
34
  recommended_workflows: [
34
35
  {
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Workspaces and Blueprints — organizational structure tools.
3
+ *
4
+ * Workspace = live folder under an Organization (owns goals + plans).
5
+ * Blueprint = dehydrated reusable shape, forks into a Workspace or Plan.
6
+ *
7
+ * v1 supports plan-scope blueprints only. See
8
+ * agent-planner/docs/WORKSPACE_BLUEPRINT_SKETCH.md for the design.
9
+ */
10
+
11
+ const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
12
+
13
+ // ─── Workspaces ──────────────────────────────────────────────────
14
+
15
+ const listWorkspacesDefinition = {
16
+ name: 'list_workspaces',
17
+ description:
18
+ "List workspaces in an organization. A workspace is a folder that owns " +
19
+ "goals + plans. Returns archived workspaces only when include_archived=true.",
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ organization_id: { type: 'string', description: "Required. Organization to scope to." },
24
+ include_archived: { type: 'boolean', default: false },
25
+ },
26
+ required: ['organization_id'],
27
+ },
28
+ };
29
+
30
+ async function listWorkspacesHandler(args, apiClient) {
31
+ const { organization_id, include_archived } = args;
32
+ try {
33
+ const data = await apiClient.workspaces.list({
34
+ organizationId: organization_id,
35
+ includeArchived: include_archived === true,
36
+ });
37
+ return formatResponse({
38
+ as_of: asOf(),
39
+ workspaces: safeArray(data.workspaces || data),
40
+ });
41
+ } catch (err) {
42
+ return errorResponse('upstream_unavailable', `list_workspaces failed: ${err.message}`);
43
+ }
44
+ }
45
+
46
+ const createWorkspaceDefinition = {
47
+ name: 'create_workspace',
48
+ description:
49
+ "Create a new workspace inside an organization. Returns the new workspace " +
50
+ "row. The slug is auto-generated from the title and de-duplicated within " +
51
+ "the org.",
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ organization_id: { type: 'string' },
56
+ title: { type: 'string' },
57
+ description: { type: 'string' },
58
+ icon: { type: 'string', description: "Optional emoji or icon token." },
59
+ slug: { type: 'string', description: "Optional. Auto-generated from title if omitted." },
60
+ },
61
+ required: ['organization_id', 'title'],
62
+ },
63
+ };
64
+
65
+ async function createWorkspaceHandler(args, apiClient) {
66
+ const { organization_id, title, description, icon, slug } = args;
67
+ try {
68
+ const ws = await apiClient.workspaces.create({
69
+ organization_id,
70
+ title,
71
+ description,
72
+ icon,
73
+ slug,
74
+ });
75
+ return formatResponse({ as_of: asOf(), workspace: ws });
76
+ } catch (err) {
77
+ const upstream = err.response?.data?.error || err.message;
78
+ return errorResponse('create_failed', `create_workspace failed: ${upstream}`);
79
+ }
80
+ }
81
+
82
+ // ─── Blueprints ──────────────────────────────────────────────────
83
+
84
+ const listBlueprintsDefinition = {
85
+ name: 'list_blueprints',
86
+ description:
87
+ "List blueprints visible to the user (owned + public/unlisted). Filter by " +
88
+ "scope ('plan' or 'workspace'), visibility, or owner_only=true.",
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ scope: { type: 'string', enum: ['plan', 'workspace'] },
93
+ visibility: { type: 'string', enum: ['private', 'public', 'unlisted'] },
94
+ owner_only: { type: 'boolean', default: false },
95
+ },
96
+ },
97
+ };
98
+
99
+ async function listBlueprintsHandler(args, apiClient) {
100
+ try {
101
+ const data = await apiClient.blueprints.list({
102
+ scope: args.scope,
103
+ visibility: args.visibility,
104
+ ownerOnly: args.owner_only === true,
105
+ });
106
+ return formatResponse({
107
+ as_of: asOf(),
108
+ blueprints: safeArray(data.blueprints || data),
109
+ });
110
+ } catch (err) {
111
+ return errorResponse('upstream_unavailable', `list_blueprints failed: ${err.message}`);
112
+ }
113
+ }
114
+
115
+ const forkBlueprintDefinition = {
116
+ name: 'fork_blueprint',
117
+ description:
118
+ "Fork a plan-scope blueprint into a target workspace. Creates a new plan " +
119
+ "inside that workspace with the blueprint's structure (nodes, " +
120
+ "dependencies, agent_instructions). All node statuses reset to " +
121
+ "'not_started'. The new plan's forked_from_blueprint_id records lineage.",
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ blueprint_id: { type: 'string' },
126
+ workspace_id: { type: 'string', description: "Target workspace the new plan will land in." },
127
+ title: { type: 'string', description: "Optional title override for the new plan." },
128
+ },
129
+ required: ['blueprint_id', 'workspace_id'],
130
+ },
131
+ };
132
+
133
+ async function forkBlueprintHandler(args, apiClient) {
134
+ const { blueprint_id, workspace_id, title } = args;
135
+ try {
136
+ const newPlan = await apiClient.blueprints.fork(blueprint_id, {
137
+ workspace_id,
138
+ title,
139
+ });
140
+ return formatResponse({
141
+ as_of: asOf(),
142
+ plan_id: newPlan.id,
143
+ workspace_id,
144
+ forked_from_blueprint_id: newPlan.forkedFromBlueprintId || blueprint_id,
145
+ title: newPlan.title,
146
+ next_step:
147
+ "Plan is in status='draft'. Open it, claim a task, or promote to 'active' once ready to execute.",
148
+ });
149
+ } catch (err) {
150
+ const upstream = err.response?.data?.error || err.message;
151
+ return errorResponse('fork_failed', `fork_blueprint failed: ${upstream}`);
152
+ }
153
+ }
154
+
155
+ const saveAsBlueprintDefinition = {
156
+ name: 'save_as_blueprint',
157
+ description:
158
+ "Snapshot a live plan as a new plan-scope blueprint. Captures structure, " +
159
+ "agent_instructions, and dependencies. Excludes run-state (statuses, " +
160
+ "claims, knowledge episodes, logs, decisions, agent assignments).",
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ plan_id: { type: 'string' },
165
+ title: { type: 'string', description: "Optional. Defaults to the source plan's title." },
166
+ description: { type: 'string' },
167
+ visibility: { type: 'string', enum: ['private', 'public', 'unlisted'], default: 'private' },
168
+ tags: { type: 'array', items: { type: 'string' } },
169
+ },
170
+ required: ['plan_id'],
171
+ },
172
+ };
173
+
174
+ async function saveAsBlueprintHandler(args, apiClient) {
175
+ const { plan_id, title, description, visibility, tags } = args;
176
+ try {
177
+ const bp = await apiClient.blueprints.saveFromPlan(plan_id, {
178
+ title,
179
+ description,
180
+ visibility,
181
+ tags,
182
+ });
183
+ return formatResponse({
184
+ as_of: asOf(),
185
+ blueprint_id: bp.id,
186
+ scope: bp.scope,
187
+ visibility: bp.visibility,
188
+ node_count: bp.payload?.nodes?.length ?? null,
189
+ dependency_count: bp.payload?.dependencies?.length ?? null,
190
+ next_step: bp.visibility === 'private'
191
+ ? "Blueprint saved privately. Share it via update visibility to 'public' or 'unlisted', or fork it directly via fork_blueprint."
192
+ : "Blueprint published. Anyone with the link (unlisted) or via discovery (public) can fork it.",
193
+ });
194
+ } catch (err) {
195
+ const upstream = err.response?.data?.error || err.message;
196
+ return errorResponse('snapshot_failed', `save_as_blueprint failed: ${upstream}`);
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ definitions: [
202
+ listWorkspacesDefinition,
203
+ createWorkspaceDefinition,
204
+ listBlueprintsDefinition,
205
+ forkBlueprintDefinition,
206
+ saveAsBlueprintDefinition,
207
+ ],
208
+ handlers: {
209
+ list_workspaces: listWorkspacesHandler,
210
+ create_workspace: createWorkspaceHandler,
211
+ list_blueprints: listBlueprintsHandler,
212
+ fork_blueprint: forkBlueprintHandler,
213
+ save_as_blueprint: saveAsBlueprintHandler,
214
+ },
215
+ };