@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.
Files changed (98) hide show
  1. package/README.md +305 -0
  2. package/config/development.js +20 -0
  3. package/config/endpoint-map.js +351 -0
  4. package/config/index.js +34 -0
  5. package/config/production.js +18 -0
  6. package/config/staging.js +18 -0
  7. package/handlers/access.js +141 -0
  8. package/handlers/activity.js +112 -0
  9. package/handlers/agents.js +95 -0
  10. package/handlers/ai-intelligence.js +55 -0
  11. package/handlers/attachments.js +30 -0
  12. package/handlers/catalogs.js +169 -0
  13. package/handlers/components.js +282 -0
  14. package/handlers/context-manifest.js +1150 -0
  15. package/handlers/decisions.js +114 -0
  16. package/handlers/designs.js +118 -0
  17. package/handlers/documents.js +227 -0
  18. package/handlers/entities.js +95 -0
  19. package/handlers/epics.js +190 -0
  20. package/handlers/facts.js +62 -0
  21. package/handlers/feature-flags.js +142 -0
  22. package/handlers/features.js +137 -0
  23. package/handlers/folders.js +127 -0
  24. package/handlers/git-context.js +917 -0
  25. package/handlers/github.js +72 -0
  26. package/handlers/graph.js +23 -0
  27. package/handlers/index.js +205 -0
  28. package/handlers/links.js +156 -0
  29. package/handlers/milestones.js +131 -0
  30. package/handlers/organizations.js +14 -0
  31. package/handlers/projects.js +122 -0
  32. package/handlers/recurring-tasks.js +33 -0
  33. package/handlers/tags.js +124 -0
  34. package/handlers/tasks.js +561 -0
  35. package/handlers/testing.js +116 -0
  36. package/handlers/todos.js +43 -0
  37. package/handlers/watchers.js +54 -0
  38. package/handlers/work-templates.js +32 -0
  39. package/index.js +175 -0
  40. package/lib/active-session.js +86 -0
  41. package/lib/auto-assign.js +93 -0
  42. package/lib/autolink.js +176 -0
  43. package/lib/changed-files.js +22 -0
  44. package/lib/env.js +45 -0
  45. package/lib/git-helpers.js +553 -0
  46. package/lib/git-utils.js +73 -0
  47. package/lib/http-client.js +164 -0
  48. package/lib/links-at-create.js +94 -0
  49. package/lib/local-cache.js +140 -0
  50. package/lib/logger.js +109 -0
  51. package/lib/manifest-loader.js +182 -0
  52. package/lib/manifest-query.js +686 -0
  53. package/lib/repo-config-dir.js +118 -0
  54. package/lib/version.js +10 -0
  55. package/lib/web-url.js +69 -0
  56. package/lib/worktree-tools.js +950 -0
  57. package/package.json +62 -0
  58. package/prompts/ai-workflow-automation.js +96 -0
  59. package/prompts/index.js +39 -0
  60. package/prompts/zephly-usage-guide-content.txt +631 -0
  61. package/prompts/zephly-usage-guide.js +119 -0
  62. package/tools/access-entity-types.js +28 -0
  63. package/tools/access.js +152 -0
  64. package/tools/activity.js +38 -0
  65. package/tools/agents.js +208 -0
  66. package/tools/ai-intelligence.js +111 -0
  67. package/tools/attachments.js +92 -0
  68. package/tools/catalogs.js +341 -0
  69. package/tools/components.js +249 -0
  70. package/tools/context-manifest.js +236 -0
  71. package/tools/decisions.js +168 -0
  72. package/tools/designs.js +222 -0
  73. package/tools/documents.js +287 -0
  74. package/tools/entities.js +223 -0
  75. package/tools/epics.js +267 -0
  76. package/tools/facts.js +70 -0
  77. package/tools/feature-flags.js +300 -0
  78. package/tools/features.js +246 -0
  79. package/tools/folders.js +122 -0
  80. package/tools/git-context.js +109 -0
  81. package/tools/github.js +172 -0
  82. package/tools/graph.js +70 -0
  83. package/tools/index.js +77 -0
  84. package/tools/link-params.js +93 -0
  85. package/tools/linkable-types.js +36 -0
  86. package/tools/links.js +199 -0
  87. package/tools/milestones.js +176 -0
  88. package/tools/organizations.js +23 -0
  89. package/tools/projects.js +172 -0
  90. package/tools/recurring-tasks.js +115 -0
  91. package/tools/tags.js +219 -0
  92. package/tools/task-item-schema.js +57 -0
  93. package/tools/task-type.js +33 -0
  94. package/tools/tasks.js +680 -0
  95. package/tools/testing.js +344 -0
  96. package/tools/todos.js +69 -0
  97. package/tools/watchers.js +81 -0
  98. 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
+ }