@ezmodo/mcp-server 0.14.4 → 0.18.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.
@@ -29,6 +29,11 @@ export const ENDPOINT_MAP = {
29
29
  'mcpSearchEpics': { route: 'mcp/v1/epics/search', method: 'POST' },
30
30
  'mcpListEpics': { route: 'mcp/v1/epics', method: 'GET' },
31
31
  'mcpGetEpic': { route: 'mcp/v1/epics/by-id', method: 'GET' },
32
+ // E-259: epic plan with revisions.
33
+ 'mcpGetEpicPlan': { route: 'mcp/v1/epics/plan', method: 'GET' },
34
+ 'mcpUpdateEpicPlan': { route: 'mcp/v1/epics/plan', method: 'PUT' },
35
+ 'mcpListEpicComments': { route: 'mcp/v1/epics/comments', method: 'GET' },
36
+ 'mcpAddEpicComment': { route: 'mcp/v1/epics/comments', method: 'POST' },
32
37
  // E-237 #2382: the epic is the fifth consumer of the grounding engine.
33
38
  'mcpGenerateEpicHowItWorks': { route: 'mcp/v1/epics/generate-how-it-works', method: 'POST' },
34
39
  'mcpApplyEpicHowItWorks': { route: 'mcp/v1/epics/apply-how-it-works', method: 'POST' },
@@ -131,27 +136,6 @@ export const ENDPOINT_MAP = {
131
136
  'mcpRemoveAccessEntry': { route: 'mcp/v1/access/entries', method: 'DELETE' },
132
137
  'mcpCheckAccess': { route: 'mcp/v1/access/check', method: 'POST' },
133
138
 
134
- // Components (project-scoped codebase areas)
135
- 'mcpCreateComponent': { route: 'mcp/v1/components', method: 'POST' },
136
- 'mcpListComponents': { route: 'mcp/v1/components', method: 'GET' },
137
- 'mcpUpdateComponent': { route: 'mcp/v1/components', method: 'PUT' },
138
- 'mcpDeleteComponent': { route: 'mcp/v1/components', method: 'DELETE' },
139
- 'mcpGetComponentStats': { route: 'mcp/v1/components/stats', method: 'GET' },
140
- 'mcpGetComponentDependencyGraph': { route: 'mcp/v1/components/dependency-graph', method: 'GET' },
141
- // E-223: screen→screen navigation edges (the screen-flow map).
142
- 'mcpGetComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'GET' },
143
- 'mcpAddComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'POST' },
144
- 'mcpRemoveComponentNavigation': { route: 'mcp/v1/components/navigation', method: 'DELETE' },
145
- // E-239 #2409: re-derive the flow map from code instead of hand-drawing it.
146
- 'mcpDeriveComponentNavigation': { route: 'mcp/v1/components/navigation/derive', method: 'POST' },
147
- 'mcpAddComponentDependency': { route: 'mcp/v1/components/dependencies', method: 'POST' },
148
- 'mcpRemoveComponentDependency': { route: 'mcp/v1/components/dependencies', method: 'DELETE' },
149
- // E-168: unified UI inventory — Components span kinds area|screen|page|component.
150
- // discover proposes page/component surfaces from the Context Manifest;
151
- // import bulk-creates surfaces (optionally nested and/or feature-linked).
152
- 'mcpDiscoverComponents': { route: 'mcp/v1/components/discover', method: 'GET' },
153
- 'mcpImportComponents': { route: 'mcp/v1/components/import', method: 'POST' },
154
-
155
139
  // Polymorphic links — single CRUD surface for any entity_links row
156
140
  // (blocked_by, relates_to) across tasks, epics, projects, documents.
157
141
  'mcpAddLink': { route: 'mcp/v1/links', method: 'POST' },
@@ -211,6 +195,8 @@ export const ENDPOINT_MAP = {
211
195
  'mcpSearchFeatures': { route: 'mcp/v1/features/search', method: 'GET' },
212
196
  'mcpLinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'POST' },
213
197
  'mcpUnlinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'DELETE' },
198
+ 'mcpListFeaturePaths': { route: 'mcp/v1/features/paths', method: 'GET' },
199
+ 'mcpSetFeaturePaths': { route: 'mcp/v1/features/paths', method: 'PUT' },
214
200
  'mcpPromoteEpicToFeature': { route: 'mcp/v1/features/promote-epic', method: 'POST' },
215
201
  'mcpGenerateHowItWorks': { route: 'mcp/v1/features/generate-how-it-works', method: 'POST' },
216
202
  'mcpApplyHowItWorks': { route: 'mcp/v1/features/apply-how-it-works', method: 'POST' },
@@ -228,6 +214,11 @@ export const ENDPOINT_MAP = {
228
214
  'mcpPromoteDecisionFromKnowledge': { route: 'mcp/v1/decisions/promote-from-knowledge', method: 'POST' },
229
215
  'mcpLinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'POST' },
230
216
  'mcpUnlinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'DELETE' },
217
+ // Decisions to make on an epic (E-259).
218
+ 'mcpAddDecisionInput': { route: 'mcp/v1/decisions/inputs', method: 'POST' },
219
+ 'mcpDecideDecision': { route: 'mcp/v1/decisions/decide', method: 'POST' },
220
+ 'mcpHoldTaskForDecision': { route: 'mcp/v1/decisions/holds', method: 'POST' },
221
+ 'mcpReleaseTaskFromDecision': { route: 'mcp/v1/decisions/holds', method: 'DELETE' },
231
222
 
232
223
  // Recurring task schedules (E-211) — "what task to create, on what cadence".
233
224
  'mcpCreateRecurringTask': { route: 'mcp/v1/recurring-tasks', method: 'POST' },
@@ -270,6 +261,13 @@ export const ENDPOINT_MAP = {
270
261
  'mcpUpdateCatalog': { route: 'mcp/v1/catalogs', method: 'PUT' },
271
262
  'mcpDeleteCatalog': { route: 'mcp/v1/catalogs', method: 'DELETE' },
272
263
  'mcpSnapshotCatalog': { route: 'mcp/v1/catalogs/snapshot', method: 'POST' },
264
+ 'mcpDiscoverScreens': { route: 'mcp/v1/screens/discover', method: 'GET' },
265
+ 'mcpImportScreens': { route: 'mcp/v1/screens/import', method: 'POST' },
266
+ 'mcpSyncScreens': { route: 'mcp/v1/screens/sync', method: 'POST' },
267
+ 'mcpListUnmappedPaths': { route: 'mcp/v1/unmapped-paths', method: 'GET' },
268
+ 'mcpAssignUnmappedPath': { route: 'mcp/v1/unmapped-paths/assign', method: 'POST' },
269
+ 'mcpDismissUnmappedPath': { route: 'mcp/v1/unmapped-paths/dismiss', method: 'POST' },
270
+ 'mcpReconcileUnmappedPaths': { route: 'mcp/v1/unmapped-paths/reconcile', method: 'POST' },
273
271
  'mcpLinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'POST' },
274
272
  'mcpUnlinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'DELETE' },
275
273
  // Item-level links (E-218) — attach work or an external URL to one catalog entry
@@ -24,8 +24,12 @@ export async function manageCatalog(args) {
24
24
  case 'link': return params.itemKey ? linkCatalogItem(params) : linkCatalogArtifact(params);
25
25
  case 'unlink': return params.itemKey ? unlinkCatalogItem(params) : unlinkCatalogArtifact(params);
26
26
  case 'snapshot': return snapshotCatalog(params);
27
+ case 'discover_screens': return discoverScreens(params);
28
+ case 'import_screens': return importScreens(params);
29
+ case 'sync_screens': return syncScreens(params);
27
30
  default:
28
- throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, or snapshot.`);
31
+ throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, snapshot, ` +
32
+ 'discover_screens, import_screens, or sync_screens.');
29
33
  }
30
34
  }
31
35
 
@@ -167,3 +171,37 @@ async function snapshotCatalog({ catalogId, mode, snapshot, upsertItems, removeK
167
171
  if (source) params.source = source;
168
172
  return callZephlyAPI('mcpSnapshotCatalog', params);
169
173
  }
174
+
175
+ // --- Screens catalog (E-258) ---
176
+
177
+ // Screens and pages the project's manifest shows that its screens catalog does
178
+ // not hold yet. Read-only.
179
+ async function discoverScreens({ projectId }) {
180
+ if (!projectId) {
181
+ throw new Error('projectId is required for discover_screens');
182
+ }
183
+ return callZephlyAPI('mcpDiscoverScreens', { projectId });
184
+ }
185
+
186
+ // Add screens to the project's screens catalog (created on first use),
187
+ // optionally linking every one to a feature.
188
+ async function importScreens({ projectId, screens, featureId }) {
189
+ if (!projectId) {
190
+ throw new Error('projectId is required for import_screens');
191
+ }
192
+ if (!Array.isArray(screens) || screens.length === 0) {
193
+ throw new Error('screens must be a non-empty array for import_screens');
194
+ }
195
+ const params = { projectId, screens };
196
+ if (featureId) params.featureId = featureId;
197
+ return callZephlyAPI('mcpImportScreens', params);
198
+ }
199
+
200
+ // Reconcile the project's screens catalog with its synced manifest and
201
+ // re-derive the screen flow map (navigates_to between catalog items).
202
+ async function syncScreens({ projectId }) {
203
+ if (!projectId) {
204
+ throw new Error('projectId is required for sync_screens');
205
+ }
206
+ return callZephlyAPI('mcpSyncScreens', { projectId });
207
+ }
@@ -374,7 +374,6 @@ async function handleEntityContext(args) {
374
374
  };
375
375
  // Map entityType+entityId to the correct graph param
376
376
  if (entityType === 'task') graphParams.taskId = entityId;
377
- else if (entityType === 'component') graphParams.componentId = entityId;
378
377
  else if (entityType === 'file') graphParams.filePath = entityId;
379
378
  else if (entityType === 'tag') graphParams.tagId = entityId;
380
379
 
@@ -389,7 +388,6 @@ async function handleEntityContext(args) {
389
388
  if (sections.includes('impact')) {
390
389
  const impactParams = { projectId, depth: args.depth };
391
390
  if (entityType === 'file') impactParams.filePath = entityId;
392
- else if (entityType === 'component') impactParams.componentId = entityId;
393
391
 
394
392
  promises.push(
395
393
  callZephlyAPI('mcpAnalyzeImpact', impactParams)
@@ -566,8 +564,6 @@ function getDefaultIncludes(entityType) {
566
564
  case 'task':
567
565
  case 'epic':
568
566
  return ['graph'];
569
- case 'component':
570
- return ['graph', 'impact'];
571
567
  case 'file':
572
568
  return ['dependencies', 'impact'];
573
569
  case 'tag':
@@ -25,8 +25,13 @@ export async function manageDecision(args) {
25
25
  case 'unlink': return unlinkDecisionArtifact(params);
26
26
  case 'supersede': return supersedeDecision(params);
27
27
  case 'promote_from_knowledge': return promoteFromKnowledge(params);
28
+ case 'add_input': return addDecisionInput(params);
29
+ case 'decide': return decideDecision(params);
30
+ case 'hold_task': return holdTask(params);
31
+ case 'release_task': return releaseTask(params);
28
32
  default:
29
- throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, supersede, or promote_from_knowledge.`);
33
+ throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, ` +
34
+ 'supersede, promote_from_knowledge, add_input, decide, hold_task, or release_task.');
30
35
  }
31
36
  }
32
37
 
@@ -48,6 +53,13 @@ export async function getDecision(args) {
48
53
  return result;
49
54
  }
50
55
 
56
+ // Decisions to make on an epic (E-259).
57
+ if (filters.epicId) {
58
+ const params = { epicId: filters.epicId };
59
+ if (filters.status) params.status = filters.status;
60
+ return callZephlyAPI('mcpListDecisions', params);
61
+ }
62
+
51
63
  // List mode
52
64
  const params = {};
53
65
  if (organizationId) params.organizationId = organizationId;
@@ -66,6 +78,10 @@ export async function getDecision(args) {
66
78
  async function createDecision(args) {
67
79
  // `links` is applied by the MCP layer after the decision exists (E-225).
68
80
  const { links, ...createArgs } = args;
81
+ // On create the server takes option labels; accept {label} objects too.
82
+ if (Array.isArray(createArgs.choices)) {
83
+ createArgs.choices = createArgs.choices.map((c) => (typeof c === 'string' ? c : c?.label));
84
+ }
69
85
  const result = await callZephlyAPI('mcpCreateDecision', createArgs);
70
86
 
71
87
  // Attach create-time links (E-225) — best effort, never fails the create.
@@ -79,6 +95,11 @@ async function createDecision(args) {
79
95
  }
80
96
 
81
97
  async function updateDecision(args) {
98
+ // On update the server takes the full list as {id, label}; a bare string is
99
+ // a new option.
100
+ if (Array.isArray(args.choices)) {
101
+ args = { ...args, choices: args.choices.map((c) => (typeof c === 'string' ? { label: c } : c)) };
102
+ }
82
103
  return callZephlyAPI('mcpUpdateDecision', args);
83
104
  }
84
105
 
@@ -112,3 +133,24 @@ async function promoteFromKnowledge({ organizationId, taskId, knowledgeId, title
112
133
  linkToId,
113
134
  });
114
135
  }
136
+
137
+ // --- Decisions to make on an epic (E-259) ---
138
+
139
+ // Record a pick. It counts for the person whose key is used, and replaces
140
+ // their earlier pick; the server records which AI made it.
141
+ async function addDecisionInput({ decisionId, choiceId, reason }) {
142
+ return callZephlyAPI('mcpAddDecisionInput', { decisionId, choiceId, reason });
143
+ }
144
+
145
+ // Decide. The server refuses anyone but the epic's owner or an editor.
146
+ async function decideDecision({ decisionId, status, choiceId, decision, rejectedReasons }) {
147
+ return callZephlyAPI('mcpDecideDecision', { decisionId, status, choiceId, decision, rejectedReasons });
148
+ }
149
+
150
+ async function holdTask({ decisionId, taskId }) {
151
+ return callZephlyAPI('mcpHoldTaskForDecision', { decisionId, taskId });
152
+ }
153
+
154
+ async function releaseTask({ decisionId, taskId }) {
155
+ return callZephlyAPI('mcpReleaseTaskFromDecision', { decisionId, taskId });
156
+ }
package/handlers/epics.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Handler functions for epic-related MCP tools
4
4
  *
5
5
  * Project-First Hierarchy: Epics belong to projects (required),
6
- * with optional component grouping and milestone linking.
6
+ * with optional milestone linking.
7
7
  *
8
8
  * Auto-assignment: When creating epics, automatically applies matching tags
9
9
  * based on content analysis against the local project cache.
@@ -188,3 +188,51 @@ export async function getEpic(args) {
188
188
  if (webUrl && result?.epic) result.epic.webUrl = webUrl;
189
189
  return result;
190
190
  }
191
+
192
+ /**
193
+ * Read an epic's plan with its current revision (E-259).
194
+ */
195
+ export async function getEpicPlan(args) {
196
+ return callZephlyAPI('mcpGetEpicPlan', args);
197
+ }
198
+
199
+ /**
200
+ * Save an epic's plan against the revision it was read at (E-259). A conflict
201
+ * is returned as a result, not thrown: it is an expected outcome when several
202
+ * people's AIs share a plan, and the agent needs the current plan and what
203
+ * changed to redo its edit.
204
+ */
205
+ export async function updateEpicPlan(args) {
206
+ try {
207
+ return await callZephlyAPI('mcpUpdateEpicPlan', args);
208
+ } catch (err) {
209
+ if (err?.code === 'PLAN_CONFLICT') {
210
+ const details = err.details || {};
211
+ return {
212
+ saved: false,
213
+ conflict: true,
214
+ message: `Someone else changed this plan since revision ${details.baseRevision}. ` +
215
+ 'Nothing was saved. Apply your change to the current plan below and save again with ' +
216
+ `baseRevision ${details.currentRevision}. Do not resend your old copy.`,
217
+ currentRevision: details.currentRevision,
218
+ changesSince: details.changesSince || [],
219
+ currentPlan: details.current || null,
220
+ };
221
+ }
222
+ throw err;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Read an epic's discussion (E-259).
228
+ */
229
+ export async function listEpicComments(args) {
230
+ return callZephlyAPI('mcpListEpicComments', args);
231
+ }
232
+
233
+ /**
234
+ * Post to an epic's discussion, or reply in a thread (E-259).
235
+ */
236
+ export async function addEpicComment(args) {
237
+ return callZephlyAPI('mcpAddEpicComment', args);
238
+ }
@@ -20,12 +20,13 @@ export async function manageFeature(args) {
20
20
  case 'delete': return deleteFeature(params);
21
21
  case 'link': return linkFeatureArtifact(params);
22
22
  case 'unlink': return unlinkFeatureArtifact(params);
23
+ case 'paths': return setFeaturePaths(params);
23
24
  case 'promote_epic': return promoteEpic(params);
24
25
  case 'generate_how_it_works': return generateHowItWorks(params);
25
26
  case 'apply_how_it_works': return applyHowItWorks(params);
26
27
  case 'apply_init': return applyInit(params);
27
28
  default:
28
- throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, promote_epic, generate_how_it_works, apply_how_it_works, or apply_init.`);
29
+ throw new Error(`Unknown action: ${action}. Expected create, update, delete, link, unlink, paths, promote_epic, generate_how_it_works, apply_how_it_works, or apply_init.`);
29
30
  }
30
31
  }
31
32
 
@@ -35,7 +36,7 @@ export async function manageFeature(args) {
35
36
  * - organizationId only → list (or tree when tree=true).
36
37
  */
37
38
  export async function getFeature(args) {
38
- const { tree, includeLinks, includeDetail, featureId, featureSlug, organizationId, ...filters } = args;
39
+ const { tree, includeLinks, includeDetail, includePaths, featureId, featureSlug, organizationId, ...filters } = args;
39
40
  const isSingleLookup = featureId || (featureSlug && organizationId);
40
41
 
41
42
  if (isSingleLookup) {
@@ -50,6 +51,10 @@ export async function getFeature(args) {
50
51
  const links = await callZephlyAPI('mcpListFeatureLinks', { featureId: lookupId });
51
52
  result.links = links?.links ?? links;
52
53
  }
54
+ if (includePaths && lookupId) {
55
+ const paths = await callZephlyAPI('mcpListFeaturePaths', { featureId: lookupId });
56
+ result.paths = paths?.paths ?? paths;
57
+ }
53
58
  return result;
54
59
  }
55
60
 
@@ -110,6 +115,12 @@ async function unlinkFeatureArtifact({ featureId, targetType, targetId }) {
110
115
  return callZephlyAPI('mcpUnlinkFeatureArtifact', { featureId, targetType, targetId });
111
116
  }
112
117
 
118
+ // Add, remove or replace the code paths a feature owns (E-258). Paths are what
119
+ // auto-link work to the feature when it touches those files.
120
+ async function setFeaturePaths({ featureId, pathsMode, paths }) {
121
+ return callZephlyAPI('mcpSetFeaturePaths', { featureId, mode: pathsMode || 'add', paths });
122
+ }
123
+
113
124
  async function promoteEpic({ epicId }) {
114
125
  return callZephlyAPI('mcpPromoteEpicToFeature', { epicId });
115
126
  }
@@ -19,7 +19,6 @@ export { isCacheFresh };
19
19
  import { getProject } from './projects.js';
20
20
  import { listRepositories } from './github.js';
21
21
  import { getOrganization } from './organizations.js';
22
- import { listComponents } from './components.js';
23
22
  import { getLogger } from '../lib/logger.js';
24
23
  import { listTags } from './tags.js';
25
24
  import { CONFIG } from '../config/index.js';
@@ -71,28 +70,6 @@ function legacyConfigNotice(legacyConfigPath) {
71
70
  };
72
71
  }
73
72
 
74
- /**
75
- * Fetch lightweight component summaries (id, name, description) for a project.
76
- * Filtered to kind='area' (E-168): this list is the coarse codebase-area set
77
- * agents use to pick a task's componentId, and it must match the web task
78
- * picker (which also filters to area). Without the filter, UI-inventory rows
79
- * (screen/page/component) would leak into task routing. Returns an empty array
80
- * on failure (non-fatal).
81
- */
82
- async function fetchComponentSummaries(projectId) {
83
- try {
84
- const result = await listComponents({ projectId, kind: 'area' });
85
- if (!result?.components) return [];
86
- return result.components.map((c) => ({
87
- id: c.id,
88
- name: c.name,
89
- description: c.description || '',
90
- }));
91
- } catch {
92
- return [];
93
- }
94
- }
95
-
96
73
  /**
97
74
  * Fetch lightweight tag summaries (id, name, color, category) for an organization.
98
75
  * Returns an empty array on failure (non-fatal).
@@ -168,9 +145,9 @@ no exceptions unless the user explicitly says to work on an existing task.
168
145
  **Single-scope work** (bug fix, small feature, config change, docs update):
169
146
  - Create a **task** with \`manage_task action:"create"\`:
170
147
  - \`projectId\` from cached context
171
- - \`componentId\` — pick the single most relevant component from the
172
- project context. Each task belongs to exactly one component.
173
- Get the list via \`get_current_project_context()\`.
148
+ - \`changedFiles\` — the paths you expect to touch; they resolve to the
149
+ features that own them
150
+ - \`links\` — the feature the work advances (find it with \`search_features\`)
174
151
  - Descriptive \`title\` and \`description\` (informed by \`get_context\`)
175
152
  - \`steps\` array with actionable steps referencing specific files
176
153
  - \`priority\` based on context (low / medium / high / urgent)
@@ -551,12 +528,11 @@ export async function getCurrentProjectContext(args) {
551
528
  workingDirectory: currentDir,
552
529
  configPath,
553
530
  allProjects: config.projects || null,
554
- components: config.components || [],
555
531
  tags: config.tags || [],
556
532
  autoGenerateTestCases: config.settings?.aiConfig?.autoGenerateTestCases || false,
557
533
  organizeResponseMode: config.settings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
558
534
  // The words this project's type uses (E-107). Cached alongside
559
- // components and tags because it changes about as often, and an
535
+ // tags because it changes about as often, and an
560
536
  // agent needs it on every session, not on a second round trip.
561
537
  // Absent means plain English — a project type with no template.
562
538
  projectType: config.projectType || null,
@@ -614,18 +590,17 @@ export async function getCurrentProjectContext(args) {
614
590
  validation.warnings.push('Could not validate organization access');
615
591
  }
616
592
 
617
- // Refresh components and tags lists
618
- const [components, tags] = await Promise.all([
619
- fetchComponentSummaries(activeProject.id),
620
- fetchTagSummaries(config.organizationId),
621
- ]);
593
+ // Refresh the tags list
594
+ const tags = await fetchTagSummaries(config.organizationId);
622
595
 
623
596
  // Update config on disk with fresh data (non-fatal)
624
597
  if (validation.projectExists && validation.organizationExists) {
625
598
  try {
626
599
  config.orgName = orgName;
627
600
  config.lastUpdatedAt = new Date().toISOString();
628
- config.components = components;
601
+ // Components were retired (E-258); drop the list a config written
602
+ // by an older server still carries.
603
+ delete config.components;
629
604
  config.tags = tags;
630
605
  config.projectType = projectType;
631
606
  config.terminology = terminology;
@@ -650,7 +625,6 @@ export async function getCurrentProjectContext(args) {
650
625
  workingDirectory: currentDir,
651
626
  configPath,
652
627
  allProjects: config.projects || null,
653
- components,
654
628
  tags,
655
629
  autoGenerateTestCases: projectSettings?.aiConfig?.autoGenerateTestCases || false,
656
630
  organizeResponseMode: projectSettings?.aiConfig?.organizeResponseMode || 'raw_snapshot',
@@ -836,11 +810,8 @@ export async function initializeProjectContext(args) {
836
810
  throw new Error(`Organization ${organizationId} not found or not accessible`);
837
811
  }
838
812
 
839
- // Step 5: Fetch components and tags
840
- const [components, tags] = await Promise.all([
841
- fetchComponentSummaries(projectId),
842
- fetchTagSummaries(organizationId),
843
- ]);
813
+ // Step 5: Fetch tags
814
+ const tags = await fetchTagSummaries(organizationId);
844
815
 
845
816
  // Step 6: Build config
846
817
  const config = {
@@ -853,7 +824,6 @@ export async function initializeProjectContext(args) {
853
824
  projectName: projectContext.project?.name || projectContext.name || 'Unknown Project',
854
825
  environment: CONFIG.environment, // staging, production, or dev
855
826
  lastUpdatedAt: new Date().toISOString(),
856
- components,
857
827
  tags,
858
828
  settings: {
859
829
  aiConfig: projectContext.settings?.aiConfig || projectContext.project?.settings?.aiConfig || null,
package/handlers/index.js CHANGED
@@ -9,12 +9,12 @@
9
9
  import * as organizationHandlers from './organizations.js';
10
10
  import * as projectHandlers from './projects.js';
11
11
  import * as epicHandlers from './epics.js';
12
- import * as componentHandlers from './components.js';
13
12
  import * as milestoneHandlers from './milestones.js';
14
13
  import * as featureHandlers from './features.js';
15
14
  import * as decisionHandlers from './decisions.js';
16
15
  import * as designHandlers from './designs.js';
17
16
  import * as catalogHandlers from './catalogs.js';
17
+ import * as unmappedPathHandlers from './unmapped-paths.js';
18
18
  import * as featureFlagHandlers from './feature-flags.js';
19
19
  import * as taskHandlers from './tasks.js';
20
20
  import * as documentHandlers from './documents.js';
@@ -59,10 +59,10 @@ export const HANDLERS = {
59
59
  search_epics: epicHandlers.searchEpics,
60
60
  list_epics: epicHandlers.listEpics,
61
61
  get_epic: epicHandlers.getEpic,
62
-
63
- // Components
64
- manage_component: componentHandlers.manageComponent,
65
- list_components: componentHandlers.listComponents,
62
+ get_epic_plan: epicHandlers.getEpicPlan,
63
+ update_epic_plan: epicHandlers.updateEpicPlan,
64
+ list_epic_comments: epicHandlers.listEpicComments,
65
+ add_epic_comment: epicHandlers.addEpicComment,
66
66
 
67
67
  // Milestones
68
68
  manage_milestone: milestoneHandlers.manageMilestone,
@@ -91,6 +91,8 @@ export const HANDLERS = {
91
91
  manage_catalog: catalogHandlers.manageCatalog,
92
92
  get_catalog: catalogHandlers.getCatalog,
93
93
  list_catalogs: catalogHandlers.listCatalogs,
94
+ list_unmapped_paths: unmappedPathHandlers.listUnmappedPaths,
95
+ resolve_unmapped: unmappedPathHandlers.resolveUnmapped,
94
96
  list_catalog_items: catalogHandlers.listCatalogItems,
95
97
  get_catalog_diff: catalogHandlers.getCatalogDiff,
96
98
 
package/handlers/links.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
- resolvePathsToComponents,
10
+ resolvePathsToFeatures,
11
11
  previewEntityLinks,
12
12
  partitionProposals,
13
13
  attachSuggestionIds,
@@ -107,20 +107,27 @@ export async function listLinks({
107
107
  }
108
108
 
109
109
  /**
110
- * resolve_links — which components own these files?
110
+ * resolve_links — which features own these files?
111
111
  *
112
112
  * The read that makes linking cheap: an agent asks once, before creating work,
113
- * and gets back the entities it should attach rather than having to know the
114
- * component inventory. Returns `unresolved` too, because a path nothing covers
115
- * is itself information — it usually means the inventory has a gap.
113
+ * and gets back the features it should attach rather than having to know which
114
+ * code paths each one owns. Returns `unmatchedPaths` too, because a path no
115
+ * feature owns is itself information — it usually means the feature map has a
116
+ * gap.
117
+ *
118
+ * Split by what the engine would do with them: a feature that solely owns the
119
+ * path links on its own; a path several features share is a choice for the
120
+ * caller to make.
116
121
  */
117
122
  export async function resolveLinks({ projectId, paths }) {
118
- const { matches, unresolved } = await resolvePathsToComponents({ projectId, paths });
123
+ const { features, unresolved } = await resolvePathsToFeatures({ projectId, paths });
119
124
  return {
120
- deterministic: matches.filter((m) => m.score >= 1),
121
- probable: matches.filter((m) => m.score < 1),
125
+ features: {
126
+ owned: features.filter((f) => !f.ambiguous),
127
+ shared: features.filter((f) => f.ambiguous),
128
+ },
122
129
  unmatchedPaths: unresolved,
123
- count: matches.length,
130
+ count: features.length,
124
131
  };
125
132
  }
126
133
 
@@ -135,12 +142,11 @@ export async function previewLinks({
135
142
  subjectType,
136
143
  subjectId,
137
144
  paths,
138
- componentId,
139
145
  epicId,
140
146
  trigger,
141
147
  }) {
142
148
  const { proposals } = await previewEntityLinks({
143
- projectId, subjectType, subjectId, paths, componentId, epicId, trigger,
149
+ projectId, subjectType, subjectId, paths, epicId, trigger,
144
150
  });
145
151
  const { autoLinked, linkSuggestions } = partitionProposals(proposals);
146
152
 
@@ -46,7 +46,7 @@ async function createProject(args) {
46
46
  // Update an existing project. Only the fields present in `args` are changed —
47
47
  // the server leaves anything unmentioned alone. This is the path that lets an
48
48
  // agent set gitUrl/gitProvider after creation, which is what commit-to-task
49
- // linking and component link resolution key off.
49
+ // linking and feature link resolution key off.
50
50
  async function updateProject(args) {
51
51
  return callZephlyAPI('mcpUpdateProject', args);
52
52
  }
package/handlers/tasks.js CHANGED
@@ -4,8 +4,9 @@
4
4
  *
5
5
  * Project-First Hierarchy: Tasks belong to projects (required), with optional epic grouping
6
6
  *
7
- * Component assignment: Each task should belong to exactly one component.
8
- * AI agents must explicitly provide componentId (from get_current_project_context).
7
+ * Code links are derived: the files a task touches (changedFiles / linkedFiles)
8
+ * resolve to the features that own those paths (E-258). Agents name the feature
9
+ * the work advances through `links`.
9
10
  * Tags are auto-assigned based on content analysis against the local project cache.
10
11
  */
11
12
 
@@ -112,7 +113,7 @@ async function createTask(args) {
112
113
  }
113
114
  }
114
115
 
115
- // Create the task (componentId should be explicitly provided by the agent)
116
+ // Create the task
116
117
  const result = await callZephlyAPI('mcpCreateTask', createArgs);
117
118
 
118
119
  // If the milestone is frozen and the operation was blocked, return guidance
@@ -159,7 +160,6 @@ async function createTask(args) {
159
160
  subjectType: 'task',
160
161
  subjectId: result.taskId,
161
162
  paths: (createArgs.linkedFiles || []).map((f) => f.path),
162
- componentId: createArgs.componentId,
163
163
  epicId: createArgs.epicId,
164
164
  });
165
165
  const { autoLinked, linkSuggestions } = partitionProposals(proposals);
@@ -191,20 +191,6 @@ async function createTask(args) {
191
191
  }
192
192
  }
193
193
 
194
- // If no component was named, include available components as a hint.
195
- //
196
- // Tests the EFFECTIVE set, not just the deprecated singular. Checking only
197
- // `componentId` meant a create that correctly used `componentIds` was told it
198
- // had named no component — while the links had in fact been written — so
199
- // agents kept issuing a redundant follow-up update to fix nothing (#2429).
200
- const namedComponents = Boolean(args.componentId)
201
- || (Array.isArray(args.componentIds) && args.componentIds.some(Boolean));
202
- if (!namedComponents && autoAssign?.availableComponents?.length) {
203
- result.warning = 'Task created without a component. Please provide componentIds '
204
- + 'for better organization. Use get_current_project_context() to see available components.';
205
- result.availableComponents = autoAssign.availableComponents;
206
- }
207
-
208
194
  // Attach create-time links (E-225) — best effort, never fails the create.
209
195
  await attachLinks(result, {
210
196
  sourceType: 'task',
@@ -282,7 +268,7 @@ function buildUntrackedDescription({ description, branch, changedFiles }) {
282
268
  * that would duplicate everything already created.
283
269
  */
284
270
  export async function bulkCreateTasks(args) {
285
- const { projectId, epicId, componentId, componentIds, tasks = [] } = args;
271
+ const { projectId, epicId, tasks = [] } = args;
286
272
 
287
273
  if (!projectId) throw new Error('projectId is required');
288
274
  if (!Array.isArray(tasks) || tasks.length === 0) {
@@ -300,8 +286,6 @@ export async function bulkCreateTasks(args) {
300
286
  const result = await callZephlyAPI('mcpBulkCreateTasks', {
301
287
  projectId,
302
288
  epicId,
303
- componentId,
304
- componentIds,
305
289
  tasks: tasks.map(({ changedFiles, linkedFiles, ...rest }) => ({
306
290
  ...rest,
307
291
  // Accept the same two spellings as manage_task so callers do not have to
@@ -344,8 +328,6 @@ export async function reportUntrackedWork(args) {
344
328
  projectId,
345
329
  title,
346
330
  description,
347
- componentId,
348
- componentIds,
349
331
  origin = 'untracked',
350
332
  discoveredDuringTaskId,
351
333
  branch,
@@ -372,8 +354,6 @@ export async function reportUntrackedWork(args) {
372
354
  status: 'in_progress',
373
355
  origin,
374
356
  ...(discoveredDuringTaskId ? { discoveredDuringTaskId } : {}),
375
- ...(componentId ? { componentId } : {}),
376
- ...(Array.isArray(componentIds) && componentIds.length > 0 ? { componentIds } : {}),
377
357
  ...(epicId ? { epicId } : {}),
378
358
  ...(allLinks.length > 0 ? { links: allLinks } : {}),
379
359
  ...(Array.isArray(changedFiles) && changedFiles.length > 0
@@ -508,8 +488,8 @@ export async function getTask(args) {
508
488
  //
509
489
  // `files` has always been optional and callers routinely omit it, which costs
510
490
  // more than it looks: without paths the task is invisible to auto-linking
511
- // forever, since the engine has nothing to resolve against the component
512
- // inventory. Measured on saltpig, 359 of 1224 tasks carrying a commit had no
491
+ // forever, since the engine has nothing to resolve against the paths features
492
+ // own. Measured on saltpig, 359 of 1224 tasks carrying a commit had no
513
493
  // linked files at all.
514
494
  //
515
495
  // The fix is derivation rather than discipline. The commit SHA is already