@ezmodo/mcp-server 0.13.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/README.md +305 -0
- package/config/development.js +20 -0
- package/config/endpoint-map.js +351 -0
- package/config/index.js +34 -0
- package/config/production.js +18 -0
- package/config/staging.js +18 -0
- package/handlers/access.js +141 -0
- package/handlers/activity.js +112 -0
- package/handlers/agents.js +95 -0
- package/handlers/ai-intelligence.js +55 -0
- package/handlers/attachments.js +30 -0
- package/handlers/catalogs.js +169 -0
- package/handlers/components.js +282 -0
- package/handlers/context-manifest.js +1150 -0
- package/handlers/decisions.js +114 -0
- package/handlers/designs.js +118 -0
- package/handlers/documents.js +227 -0
- package/handlers/entities.js +95 -0
- package/handlers/epics.js +190 -0
- package/handlers/facts.js +62 -0
- package/handlers/feature-flags.js +142 -0
- package/handlers/features.js +137 -0
- package/handlers/folders.js +127 -0
- package/handlers/git-context.js +917 -0
- package/handlers/github.js +72 -0
- package/handlers/graph.js +23 -0
- package/handlers/index.js +205 -0
- package/handlers/links.js +156 -0
- package/handlers/milestones.js +131 -0
- package/handlers/organizations.js +14 -0
- package/handlers/projects.js +122 -0
- package/handlers/recurring-tasks.js +33 -0
- package/handlers/tags.js +124 -0
- package/handlers/tasks.js +561 -0
- package/handlers/testing.js +116 -0
- package/handlers/todos.js +43 -0
- package/handlers/watchers.js +54 -0
- package/handlers/work-templates.js +32 -0
- package/index.js +175 -0
- package/lib/active-session.js +86 -0
- package/lib/auto-assign.js +93 -0
- package/lib/autolink.js +176 -0
- package/lib/changed-files.js +22 -0
- package/lib/env.js +45 -0
- package/lib/git-helpers.js +553 -0
- package/lib/git-utils.js +73 -0
- package/lib/http-client.js +164 -0
- package/lib/links-at-create.js +94 -0
- package/lib/local-cache.js +140 -0
- package/lib/logger.js +109 -0
- package/lib/manifest-loader.js +182 -0
- package/lib/manifest-query.js +686 -0
- package/lib/repo-config-dir.js +118 -0
- package/lib/version.js +10 -0
- package/lib/web-url.js +69 -0
- package/lib/worktree-tools.js +950 -0
- package/package.json +62 -0
- package/prompts/ai-workflow-automation.js +96 -0
- package/prompts/index.js +39 -0
- package/prompts/zephly-usage-guide-content.txt +631 -0
- package/prompts/zephly-usage-guide.js +119 -0
- package/tools/access-entity-types.js +28 -0
- package/tools/access.js +152 -0
- package/tools/activity.js +38 -0
- package/tools/agents.js +208 -0
- package/tools/ai-intelligence.js +111 -0
- package/tools/attachments.js +92 -0
- package/tools/catalogs.js +341 -0
- package/tools/components.js +249 -0
- package/tools/context-manifest.js +236 -0
- package/tools/decisions.js +168 -0
- package/tools/designs.js +222 -0
- package/tools/documents.js +287 -0
- package/tools/entities.js +223 -0
- package/tools/epics.js +267 -0
- package/tools/facts.js +70 -0
- package/tools/feature-flags.js +300 -0
- package/tools/features.js +246 -0
- package/tools/folders.js +122 -0
- package/tools/git-context.js +109 -0
- package/tools/github.js +172 -0
- package/tools/graph.js +70 -0
- package/tools/index.js +77 -0
- package/tools/link-params.js +93 -0
- package/tools/linkable-types.js +36 -0
- package/tools/links.js +199 -0
- package/tools/milestones.js +176 -0
- package/tools/organizations.js +23 -0
- package/tools/projects.js +172 -0
- package/tools/recurring-tasks.js +115 -0
- package/tools/tags.js +219 -0
- package/tools/task-item-schema.js +57 -0
- package/tools/task-type.js +33 -0
- package/tools/tasks.js +680 -0
- package/tools/testing.js +344 -0
- package/tools/todos.js +69 -0
- package/tools/watchers.js +81 -0
- package/tools/work-templates.js +96 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Handlers
|
|
3
|
+
* Handler functions for component-related MCP tools
|
|
4
|
+
*
|
|
5
|
+
* Components are project-scoped entries in the unified UI inventory (E-168).
|
|
6
|
+
* A single Component concept spans four kinds:
|
|
7
|
+
* - area — coarse codebase module (api, web, mobile); the legacy meaning
|
|
8
|
+
* - screen — a mobile / Flutter screen
|
|
9
|
+
* - page — a web route / page
|
|
10
|
+
* - component — a reusable UI component
|
|
11
|
+
* Components self-nest via parentComponentId (e.g. web → Sprint Board → TaskCard)
|
|
12
|
+
* and carry sourcePath / route / framework. The task component-picker uses
|
|
13
|
+
* kind=area. This is ONE concept — there is no separate "UI surface" entity.
|
|
14
|
+
*
|
|
15
|
+
* list_components uses local cache for simple requests, falling back to API.
|
|
16
|
+
* Write operations (create, update, delete) invalidate the cache.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
20
|
+
import { getCachedComponents, updateCacheSections, invalidateCacheSection } from '../lib/local-cache.js';
|
|
21
|
+
import { attachLinks, applyLinks } from '../lib/links-at-create.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dispatch manage_component actions to the appropriate handler
|
|
25
|
+
*/
|
|
26
|
+
export async function manageComponent(args) {
|
|
27
|
+
const { action, ...params } = args;
|
|
28
|
+
switch (action) {
|
|
29
|
+
case 'create': return createComponent(params);
|
|
30
|
+
case 'update': return updateComponent(params);
|
|
31
|
+
case 'delete': return deleteComponent(params);
|
|
32
|
+
case 'add_dependency': return addComponentDependency(params);
|
|
33
|
+
case 'remove_dependency': return removeComponentDependency(params);
|
|
34
|
+
case 'add_navigation': return addComponentNavigation(params);
|
|
35
|
+
case 'remove_navigation': return removeComponentNavigation(params);
|
|
36
|
+
case 'derive_navigation': return deriveComponentNavigation(params);
|
|
37
|
+
case 'discover': return discoverComponents(params);
|
|
38
|
+
case 'import': return importComponents(params);
|
|
39
|
+
default: throw new Error(`Unknown action: ${action}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* List components with optional enrichment data.
|
|
45
|
+
* Uses local cache when available and fresh for basic list, falls back to API.
|
|
46
|
+
*/
|
|
47
|
+
export async function listComponents(args) {
|
|
48
|
+
const { componentId, componentSlug, include, ...rest } = args;
|
|
49
|
+
const isSingleLookup = componentId || componentSlug;
|
|
50
|
+
const includes = include || [];
|
|
51
|
+
|
|
52
|
+
// Timeline is NOT a supported include. It is rejected here — loudly, and on
|
|
53
|
+
// every path — rather than dropped, because the API never had the data:
|
|
54
|
+
// nothing populates Component.CachedTimeline (there is no cached_timeline
|
|
55
|
+
// column, and repository_postgres.go neither reads nor writes the field), so
|
|
56
|
+
// Service.GetComponentTimeline always returns a zeroed struct. The single
|
|
57
|
+
// lookup therefore did not "work" while lists silently degraded; it answered
|
|
58
|
+
// all-zeros, which reads as a real empty result and is the worse failure of
|
|
59
|
+
// the two. Removing it from the schema enum is not enough on its own — a
|
|
60
|
+
// client that ignores the enum must still get an error, not a plain list.
|
|
61
|
+
if (includes.includes('timeline')) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'include:["timeline"] is not supported: component timeline data is not computed by the ' +
|
|
64
|
+
'API, so it would return all zeros rather than real data. Use include:["stats"] for task ' +
|
|
65
|
+
'counts (per component with componentId/componentSlug, or project-wide without one).'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Stats. With an identifier: that component. Without one: every component in
|
|
70
|
+
// the project (the endpoint takes the identifier as optional). This branch
|
|
71
|
+
// used to require isSingleLookup, so `include:["stats"]` on a list fell
|
|
72
|
+
// through to the plain list and the stats request vanished without an error.
|
|
73
|
+
if (includes.includes('stats')) {
|
|
74
|
+
return getComponentStats({ projectId: rest.projectId, componentId, componentSlug });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Navigation graph (project-wide screen→screen edges, E-223).
|
|
78
|
+
if (includes.includes('navigation')) {
|
|
79
|
+
return getComponentNavigation({ projectId: rest.projectId });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Dependency graph (project-wide). The kind filter must be forwarded: this
|
|
83
|
+
// branch bypasses the plain list entirely, so dropping `kind` here silently
|
|
84
|
+
// returned the WHOLE inventory to a caller who asked for one kind.
|
|
85
|
+
if (includes.includes('dependency_graph')) {
|
|
86
|
+
const graphArgs = { projectId: rest.projectId };
|
|
87
|
+
if (rest.kind) graphArgs.kind = rest.kind;
|
|
88
|
+
return getComponentDependencyGraph(graphArgs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Basic list — use cache if available. Skip the cache when a kind filter is
|
|
92
|
+
// set (E-168): the cache holds the full inventory, not kind-filtered subsets.
|
|
93
|
+
if (rest.projectId && !isSingleLookup && !rest.kind) {
|
|
94
|
+
const cached = await getCachedComponents(rest.projectId);
|
|
95
|
+
if (cached !== null) {
|
|
96
|
+
return { components: cached, cached: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The identifier must be forwarded: it is destructured out of `rest` above,
|
|
101
|
+
// so without this a single lookup fell through to a plain list and returned
|
|
102
|
+
// the WHOLE inventory to a caller who asked for one component — the same
|
|
103
|
+
// dropped-filter shape as the `kind` bug, and just as silent.
|
|
104
|
+
const listArgs = { ...rest };
|
|
105
|
+
if (componentId) listArgs.componentId = componentId;
|
|
106
|
+
if (componentSlug) listArgs.componentSlug = componentSlug;
|
|
107
|
+
|
|
108
|
+
const result = await callZephlyAPI('mcpListComponents', listArgs);
|
|
109
|
+
|
|
110
|
+
// Cache the results for future use. Only cache the FULL inventory — neither a
|
|
111
|
+
// kind-filtered result (e.g. kind='area' from the project-context path) nor a
|
|
112
|
+
// single lookup may overwrite the cache, or an unfiltered list_components
|
|
113
|
+
// would then read back a subset (mirrors the cache-read skip above). The
|
|
114
|
+
// single-lookup guard matters as of the fix above: while the identifier was
|
|
115
|
+
// being dropped this call returned the full list, so caching it was harmless.
|
|
116
|
+
if (rest.projectId && !isSingleLookup && !rest.kind && result?.components) {
|
|
117
|
+
const summaries = result.components.map((c) => ({
|
|
118
|
+
id: c.id,
|
|
119
|
+
name: c.name,
|
|
120
|
+
description: c.description || '',
|
|
121
|
+
}));
|
|
122
|
+
await updateCacheSections({ components: summaries });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create a new component
|
|
130
|
+
*/
|
|
131
|
+
async function createComponent(args) {
|
|
132
|
+
// `links` is applied by the MCP layer after the component exists (E-225).
|
|
133
|
+
const { links, ...createArgs } = args;
|
|
134
|
+
const result = await callZephlyAPI('mcpCreateComponent', createArgs);
|
|
135
|
+
await invalidateCacheSection('components');
|
|
136
|
+
|
|
137
|
+
// Attach create-time links (E-225) — best effort, never fails the create.
|
|
138
|
+
await attachLinks(result, {
|
|
139
|
+
sourceType: 'component',
|
|
140
|
+
sourceId: result?.componentId,
|
|
141
|
+
links,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Update an existing component
|
|
149
|
+
*/
|
|
150
|
+
async function updateComponent(args) {
|
|
151
|
+
const result = await callZephlyAPI('mcpUpdateComponent', args);
|
|
152
|
+
await invalidateCacheSection('components');
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Delete a component
|
|
158
|
+
*/
|
|
159
|
+
async function deleteComponent(args) {
|
|
160
|
+
const result = await callZephlyAPI('mcpDeleteComponent', args);
|
|
161
|
+
await invalidateCacheSection('components');
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get stats for a component
|
|
167
|
+
*/
|
|
168
|
+
async function getComponentStats(args) {
|
|
169
|
+
return callZephlyAPI('mcpGetComponentStats', args);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Add a dependency between two components
|
|
174
|
+
*/
|
|
175
|
+
async function addComponentDependency(args) {
|
|
176
|
+
return callZephlyAPI('mcpAddComponentDependency', args);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Remove a dependency between two components
|
|
181
|
+
*/
|
|
182
|
+
async function removeComponentDependency(args) {
|
|
183
|
+
return callZephlyAPI('mcpRemoveComponentDependency', args);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get the full dependency graph for a project's components
|
|
188
|
+
*/
|
|
189
|
+
async function getComponentDependencyGraph(args) {
|
|
190
|
+
return callZephlyAPI('mcpGetComponentDependencyGraph', args);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a screen→screen navigation edge (E-223). Endpoints may be given as IDs or
|
|
195
|
+
* slugs. Idempotent — re-adding an existing edge is a no-op.
|
|
196
|
+
*/
|
|
197
|
+
async function addComponentNavigation(args) {
|
|
198
|
+
return callZephlyAPI('mcpAddComponentNavigation', args);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Remove a screen→screen navigation edge (E-223). Idempotent.
|
|
203
|
+
*/
|
|
204
|
+
async function removeComponentNavigation(args) {
|
|
205
|
+
return callZephlyAPI('mcpRemoveComponentNavigation', args);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Re-derive the screen-flow map from the synced Context Manifest (E-239 #2409).
|
|
210
|
+
*
|
|
211
|
+
* Idempotent, and it cannot destroy hand-drawn work: it reconciles only edges
|
|
212
|
+
* still at origin auto/inferred under its own rules, so a human's edge — or one
|
|
213
|
+
* they promoted — is left alone and reported as `spared`.
|
|
214
|
+
*
|
|
215
|
+
* Returns { result: { derived, byRule, retracted, spared, unresolved }, projectId }.
|
|
216
|
+
* `unresolved` names navigation references no component matched, which is how a
|
|
217
|
+
* stale inventory shows itself instead of the map silently shrinking.
|
|
218
|
+
*/
|
|
219
|
+
async function deriveComponentNavigation({ projectId }) {
|
|
220
|
+
return callZephlyAPI('mcpDeriveComponentNavigation', { projectId });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read the project's navigation edges (E-223) so an agent can see the existing
|
|
225
|
+
* flow before writing to it. Returns { edges, projectId }.
|
|
226
|
+
*/
|
|
227
|
+
async function getComponentNavigation(args) {
|
|
228
|
+
return callZephlyAPI('mcpGetComponentNavigation', args);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Discover candidate UI surfaces (page / component kinds) for a project from the
|
|
233
|
+
* Context Manifest that are not yet in the inventory (E-168). Manifest-assisted;
|
|
234
|
+
* returns { surfaces: [{kind,name,sourcePath,route,framework,summary}], projectId, count }.
|
|
235
|
+
*/
|
|
236
|
+
async function discoverComponents({ projectId }) {
|
|
237
|
+
return callZephlyAPI('mcpDiscoverComponents', { projectId });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Bulk-import UI surfaces as components (E-168). Each surface becomes a component,
|
|
242
|
+
* optionally nested under parentComponentId and/or linked to featureId.
|
|
243
|
+
* Returns { imported: [{componentId,slug,name,kind,sourcePath,linked}], count }.
|
|
244
|
+
*/
|
|
245
|
+
async function importComponents({ projectId, parentComponentId, featureId, surfaces }) {
|
|
246
|
+
// Per-surface `links` are applied by the MCP layer after import, so a 90-screen
|
|
247
|
+
// import can carry its links in one call (E-225) instead of 90 manage_link
|
|
248
|
+
// round trips.
|
|
249
|
+
const list = Array.isArray(surfaces) ? surfaces : [];
|
|
250
|
+
const body = {
|
|
251
|
+
projectId,
|
|
252
|
+
surfaces: list.map(({ links, ...surface }) => surface),
|
|
253
|
+
};
|
|
254
|
+
if (parentComponentId) body.parentComponentId = parentComponentId;
|
|
255
|
+
if (featureId) body.featureId = featureId;
|
|
256
|
+
const result = await callZephlyAPI('mcpImportComponents', body);
|
|
257
|
+
await invalidateCacheSection('components');
|
|
258
|
+
|
|
259
|
+
// Match each imported component back to the surface that asked for links.
|
|
260
|
+
// Matching on name + sourcePath rather than index because the API is free to
|
|
261
|
+
// skip surfaces that already exist.
|
|
262
|
+
const imported = Array.isArray(result?.imported) ? result.imported : [];
|
|
263
|
+
const linkResults = [];
|
|
264
|
+
for (const surface of list) {
|
|
265
|
+
if (!Array.isArray(surface?.links) || surface.links.length === 0) continue;
|
|
266
|
+
const match = imported.find(
|
|
267
|
+
(c) => c?.name === surface.name && (!surface.sourcePath || c?.sourcePath === surface.sourcePath),
|
|
268
|
+
);
|
|
269
|
+
if (!match?.componentId) continue;
|
|
270
|
+
const outcome = await applyLinks({
|
|
271
|
+
sourceType: 'component',
|
|
272
|
+
sourceId: match.componentId,
|
|
273
|
+
links: surface.links,
|
|
274
|
+
});
|
|
275
|
+
linkResults.push({ componentId: match.componentId, ...outcome });
|
|
276
|
+
}
|
|
277
|
+
if (linkResults.length > 0 && result && typeof result === 'object') {
|
|
278
|
+
result.links = linkResults;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|