@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.
- package/config/endpoint-map.js +19 -21
- package/handlers/catalogs.js +39 -1
- package/handlers/context-manifest.js +0 -4
- package/handlers/decisions.js +43 -1
- package/handlers/epics.js +49 -1
- package/handlers/features.js +13 -2
- package/handlers/git-context.js +11 -41
- package/handlers/index.js +7 -5
- package/handlers/links.js +17 -11
- package/handlers/projects.js +1 -1
- package/handlers/tasks.js +7 -27
- package/handlers/unmapped-paths.js +60 -0
- package/lib/auto-assign.js +3 -22
- package/lib/autolink.js +11 -10
- package/lib/http-client.js +6 -0
- package/lib/instructions.generated.js +2 -2
- package/lib/local-cache.js +3 -17
- package/lib/remote-tools.js +6 -2
- package/lib/version.js +1 -1
- package/package.json +5 -5
- package/prompts/commands.generated.js +2 -2
- package/tools/catalogs.js +32 -5
- package/tools/context-manifest.js +2 -2
- package/tools/decisions.js +86 -8
- package/tools/epics.js +158 -6
- package/tools/features.js +41 -2
- package/tools/git-context.js +5 -5
- package/tools/graph.js +1 -1
- package/tools/index.js +2 -2
- package/tools/linkable-types.js +0 -1
- package/tools/links.js +7 -5
- package/tools/projects.js +2 -2
- package/tools/recurring-tasks.js +0 -1
- package/tools/task-item-schema.js +0 -6
- package/tools/tasks.js +9 -44
- package/tools/todos.js +0 -4
- package/tools/unmapped-paths.js +72 -0
- package/tools/work-templates.js +0 -2
- package/handlers/components.js +0 -282
- package/tools/components.js +0 -249
package/config/endpoint-map.js
CHANGED
|
@@ -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
|
package/handlers/catalogs.js
CHANGED
|
@@ -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,
|
|
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':
|
package/handlers/decisions.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
+
}
|
package/handlers/features.js
CHANGED
|
@@ -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
|
}
|
package/handlers/git-context.js
CHANGED
|
@@ -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
|
-
- \`
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
//
|
|
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
|
|
618
|
-
const
|
|
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
|
-
|
|
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
|
|
840
|
-
const
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
114
|
-
*
|
|
115
|
-
* is itself information — it usually means the
|
|
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 {
|
|
123
|
+
const { features, unresolved } = await resolvePathsToFeatures({ projectId, paths });
|
|
119
124
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
features: {
|
|
126
|
+
owned: features.filter((f) => !f.ambiguous),
|
|
127
|
+
shared: features.filter((f) => f.ambiguous),
|
|
128
|
+
},
|
|
122
129
|
unmatchedPaths: unresolved,
|
|
123
|
-
count:
|
|
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,
|
|
149
|
+
projectId, subjectType, subjectId, paths, epicId, trigger,
|
|
144
150
|
});
|
|
145
151
|
const { autoLinked, linkSuggestions } = partitionProposals(proposals);
|
|
146
152
|
|
package/handlers/projects.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
512
|
-
//
|
|
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
|