@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,351 @@
1
+ /**
2
+ * Endpoint Mapping: Cloud Functions → Go API
3
+ * Maps Cloud Functions endpoint names to Go API routes and HTTP methods
4
+ *
5
+ * Unified Hierarchy (Phase 6 - Simplified Data Model):
6
+ * - Projects - long-lived containers (primary)
7
+ * - Epics - related tasks grouped (project-scoped milestones)
8
+ */
9
+
10
+ export const ENDPOINT_MAP = {
11
+ // Organizations
12
+ 'mcpListOrganizations': { route: 'mcp/v1/organizations', method: 'GET' },
13
+ 'mcpGetDefaultOrganization': { route: 'mcp/v1/organizations/default', method: 'GET' },
14
+
15
+ // Projects (primary)
16
+ 'mcpListProjects': { route: 'mcp/v1/projects', method: 'GET' },
17
+ 'mcpSearchProjects': { route: 'mcp/v1/projects/search', method: 'POST' },
18
+ 'mcpGetProjectContext': { route: 'mcp/v1/projects/context', method: 'GET' },
19
+ 'mcpGetProjectStory': { route: 'mcp/v1/projects/story', method: 'POST' },
20
+ 'mcpCreateProject': { route: 'mcp/v1/projects', method: 'POST' },
21
+ 'mcpUpdateProject': { route: 'mcp/v1/projects', method: 'PUT' },
22
+ 'mcpGenerateProjectHowItWorks': { route: 'mcp/v1/projects/generate-how-it-works', method: 'POST' },
23
+ 'mcpApplyProjectHowItWorks': { route: 'mcp/v1/projects/apply-how-it-works', method: 'POST' },
24
+
25
+ // Epics (project-scoped milestones)
26
+ 'mcpCreateEpic': { route: 'mcp/v1/epics', method: 'POST' },
27
+ 'mcpCreateEpicWithTasks': { route: 'mcp/v1/epics/with-tasks', method: 'POST' },
28
+ 'mcpUpdateEpic': { route: 'mcp/v1/epics', method: 'PUT' },
29
+ 'mcpSearchEpics': { route: 'mcp/v1/epics/search', method: 'POST' },
30
+ 'mcpListEpics': { route: 'mcp/v1/epics', method: 'GET' },
31
+ 'mcpGetEpic': { route: 'mcp/v1/epics/by-id', method: 'GET' },
32
+ // E-237 #2382: the epic is the fifth consumer of the grounding engine.
33
+ 'mcpGenerateEpicHowItWorks': { route: 'mcp/v1/epics/generate-how-it-works', method: 'POST' },
34
+ 'mcpApplyEpicHowItWorks': { route: 'mcp/v1/epics/apply-how-it-works', method: 'POST' },
35
+
36
+ // Tasks
37
+ 'mcpCreateTask': { route: 'mcp/v1/tasks', method: 'POST' },
38
+ 'mcpBulkCreateTasks': { route: 'mcp/v1/tasks/bulk', method: 'POST' },
39
+ 'mcpUpdateTask': { route: 'mcp/v1/tasks', method: 'PUT' },
40
+ 'mcpCompleteTask': { route: 'mcp/v1/tasks/complete', method: 'POST' },
41
+ 'mcpDeferTask': { route: 'mcp/v1/tasks/defer', method: 'POST' },
42
+ 'mcpSearchTasks': { route: 'mcp/v1/tasks/search', method: 'POST' },
43
+ 'mcpSemanticTaskSearch': { route: 'mcp/v1/tasks/semantic-search', method: 'POST' },
44
+ 'mcpGetTask': { route: 'mcp/v1/tasks/by-id', method: 'GET' },
45
+ 'mcpAddTaskContext': { route: 'mcp/v1/tasks/context', method: 'POST' },
46
+ 'mcpLinkCommitToTask': { route: 'mcp/v1/tasks/link-commit', method: 'POST' },
47
+ 'mcpUnlinkCommitFromTask': { route: 'mcp/v1/tasks/unlink-commit', method: 'POST' },
48
+ 'mcpGetTaskCommits': { route: 'mcp/v1/tasks/commits', method: 'GET' },
49
+ 'mcpGenerateTaskHowItWorks': { route: 'mcp/v1/tasks/generate-how-it-works', method: 'POST' },
50
+ 'mcpApplyTaskHowItWorks': { route: 'mcp/v1/tasks/apply-how-it-works', method: 'POST' },
51
+
52
+ // Watching / subscriptions and the notification inbox (E-41)
53
+ 'mcpManageWatch': { route: 'mcp/v1/watch', method: 'POST' },
54
+ 'mcpListWatched': { route: 'mcp/v1/watching', method: 'GET' },
55
+ 'mcpListNotifications': { route: 'mcp/v1/notifications', method: 'GET' },
56
+
57
+ // Documentation
58
+ 'mcpGetDocumentation': { route: 'mcp/v1/documents', method: 'GET' },
59
+ 'mcpGetDocument': { route: 'mcp/v1/documents/by-id', method: 'GET' },
60
+ 'mcpCreateDocument': { route: 'mcp/v1/documents', method: 'POST' },
61
+ 'mcpUpdateDocument': { route: 'mcp/v1/documents', method: 'PUT' },
62
+ // Org-scoped (project-less) document listing (E-195)
63
+ 'mcpListOrgDocuments': { route: 'mcp/v1/org-documents', method: 'GET' },
64
+
65
+ // Document Versions (list/get only — create not yet implemented in Go API)
66
+ 'mcpListDocumentVersions': { route: 'mcp/v1/document-versions', method: 'GET' },
67
+ 'mcpGetDocumentVersion': { route: 'mcp/v1/document-versions/by-number', method: 'GET' },
68
+
69
+ // Document Templates
70
+ 'mcpListDocumentTemplates': { route: 'mcp/v1/document-templates', method: 'GET' },
71
+ 'mcpGetDocumentTemplate': { route: 'mcp/v1/document-templates/by-id', method: 'GET' },
72
+ 'mcpCreateDocumentTemplate': { route: 'mcp/v1/document-templates', method: 'POST' },
73
+ 'mcpDeleteDocumentTemplate': { route: 'mcp/v1/document-templates', method: 'DELETE' },
74
+
75
+ // Attachments (read/manage only — E-21 #158)
76
+ 'mcpListAttachments': { route: 'mcp/v1/attachments', method: 'GET' },
77
+ 'mcpGetAttachmentUrl': { route: 'mcp/v1/attachments/download-url', method: 'POST' },
78
+ 'mcpDeleteAttachment': { route: 'mcp/v1/attachments', method: 'DELETE' },
79
+
80
+ // Document Folders
81
+ 'mcpListFolders': { route: 'mcp/v1/folders', method: 'GET' },
82
+ 'mcpGetFolderTree': { route: 'mcp/v1/folders/tree', method: 'GET' },
83
+ 'mcpCreateFolder': { route: 'mcp/v1/folders', method: 'POST' },
84
+ 'mcpUpdateFolder': { route: 'mcp/v1/folders', method: 'PUT' },
85
+ 'mcpMoveFolder': { route: 'mcp/v1/folders/move', method: 'POST' },
86
+ 'mcpDeleteFolder': { route: 'mcp/v1/folders', method: 'DELETE' },
87
+ // Org reserved areas (Goals, Features, ADRs, How it works) — ensures + lists (E-195)
88
+ 'mcpGetOrgAreas': { route: 'mcp/v1/org-areas', method: 'GET' },
89
+
90
+ // Goals
91
+ 'mcpCreateGoal': { route: 'mcp/v1/goals', method: 'POST' },
92
+ 'mcpGetGoal': { route: 'mcp/v1/goals/by-id', method: 'GET' },
93
+ 'mcpUpdateGoal': { route: 'mcp/v1/goals', method: 'PUT' },
94
+ 'mcpListGoals': { route: 'mcp/v1/goals', method: 'GET' },
95
+ 'mcpDeleteGoal': { route: 'mcp/v1/goals', method: 'DELETE' },
96
+ 'mcpGenerateGoalHowItWorks': { route: 'mcp/v1/goals/generate-how-it-works', method: 'POST' },
97
+ 'mcpApplyGoalHowItWorks': { route: 'mcp/v1/goals/apply-how-it-works', method: 'POST' },
98
+
99
+ // Tags (organization-scoped categorization)
100
+ 'mcpCreateTag': { route: 'mcp/v1/tags', method: 'POST' },
101
+ 'mcpListTags': { route: 'mcp/v1/tags', method: 'GET' },
102
+ 'mcpGetTag': { route: 'mcp/v1/tags/by-id', method: 'GET' },
103
+ 'mcpUpdateTag': { route: 'mcp/v1/tags', method: 'PUT' },
104
+ 'mcpDeleteTag': { route: 'mcp/v1/tags', method: 'DELETE' },
105
+ 'mcpMergeTags': { route: 'mcp/v1/tags/merge', method: 'POST' },
106
+ 'mcpBulkTagEntities': { route: 'mcp/v1/tags/bulk', method: 'POST' },
107
+ 'mcpFindEntitiesByTags': { route: 'mcp/v1/tags/find-entities', method: 'POST' },
108
+ 'mcpSuggestTags': { route: 'mcp/v1/tags/suggest', method: 'POST' }, // AI-powered semantic tag suggestions
109
+
110
+ // Teams
111
+ 'mcpCreateTeam': { route: 'mcp/v1/teams', method: 'POST' },
112
+
113
+ // Organization analysis
114
+ 'mcpAnalyzeProjectOrganization': { route: 'mcp/v1/organization/analyze', method: 'POST' },
115
+
116
+ // AI Intelligence endpoints
117
+ 'mcpGetProjectInsights': { route: 'mcp/v1/insights/project', method: 'POST' },
118
+ 'mcpSuggestNextActions': { route: 'mcp/v1/insights/suggest-next', method: 'POST' },
119
+ 'mcpAnalyzeDependencyGraph': { route: 'mcp/v1/insights/dependency-graph', method: 'POST' },
120
+ 'mcpAnalyzeBuildFailure': { route: 'mcp/v1/insights/build-failure', method: 'POST' },
121
+ 'mcpPredictDeploymentRisk': { route: 'mcp/v1/insights/deployment-risk', method: 'POST' },
122
+
123
+ 'mcpEstimateTask': { route: 'mcp/v1/intelligence/estimate', method: 'GET' },
124
+ 'mcpGetInferredDependencies': { route: 'mcp/v1/intelligence/inferred-dependencies', method: 'GET' },
125
+
126
+ // Entity access control (#2170). Reads take the optional read:access scope;
127
+ // the three mutations require the dedicated write:access scope server-side.
128
+ 'mcpGetAccess': { route: 'mcp/v1/access', method: 'GET' },
129
+ 'mcpUpdateAccess': { route: 'mcp/v1/access', method: 'PUT' },
130
+ 'mcpAddAccessEntry': { route: 'mcp/v1/access/entries', method: 'POST' },
131
+ 'mcpRemoveAccessEntry': { route: 'mcp/v1/access/entries', method: 'DELETE' },
132
+ 'mcpCheckAccess': { route: 'mcp/v1/access/check', method: 'POST' },
133
+
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
+ // Polymorphic links — single CRUD surface for any entity_links row
156
+ // (blocked_by, relates_to) across tasks, epics, projects, documents.
157
+ 'mcpAddLink': { route: 'mcp/v1/links', method: 'POST' },
158
+ 'mcpRemoveLink': { route: 'mcp/v1/links', method: 'DELETE' },
159
+ 'mcpListLinks': { route: 'mcp/v1/links', method: 'GET' },
160
+ 'mcpVerifyLink': { route: 'mcp/v1/links/verify', method: 'POST' },
161
+ // E-225 read-only autolink surfaces. POST rather than GET because both take a
162
+ // list of paths: a query string flattens an array to a comma-joined value,
163
+ // and file paths may legitimately contain commas.
164
+ 'mcpResolvePaths': { route: 'mcp/v1/links/resolve-paths', method: 'POST' },
165
+ 'mcpPreviewLinks': { route: 'mcp/v1/links/preview', method: 'POST' },
166
+ // No 'mcpLinkSuggestions' here on purpose: mcp/v1/links/suggestions reads the
167
+ // same agent_suggestions rows as mcpListAgentSuggestions below, which already
168
+ // has a tool. Two ways to read one queue is how they drift apart (#2297).
169
+
170
+ // E-150 background-agent control plane. Suggestions live in
171
+ // agent_suggestions; accept/reject mutate state via core/agents.Dispatcher.
172
+ 'mcpListAgentSuggestions': { route: 'mcp/v1/agent-suggestions', method: 'GET' },
173
+ 'mcpAcceptAgentSuggestion': { route: 'mcp/v1/agent-suggestions/accept', method: 'POST' },
174
+ 'mcpRejectAgentSuggestion': { route: 'mcp/v1/agent-suggestions/reject', method: 'POST' },
175
+ 'mcpRunAgentNow': { route: 'mcp/v1/agent-runs', method: 'POST' },
176
+ 'mcpConfigureAgent': { route: 'mcp/v1/agent-settings', method: 'PUT' },
177
+
178
+ // Project facts — long-lived knowledge entries (decisions, conventions,
179
+ // reference info). The MCP handlers in handlers/facts.js were already
180
+ // calling these aliases; adding the route mappings here so they don't
181
+ // hit "Unknown endpoint".
182
+ 'mcpCreateFact': { route: 'mcp/v1/facts', method: 'POST' },
183
+ 'mcpListFacts': { route: 'mcp/v1/facts', method: 'GET' },
184
+ 'mcpUpdateFact': { route: 'mcp/v1/facts', method: 'PUT' },
185
+ 'mcpDeleteFact': { route: 'mcp/v1/facts', method: 'DELETE' },
186
+
187
+ // Milestones (version releases and initiatives)
188
+ 'mcpCreateMilestone': { route: 'mcp/v1/milestones', method: 'POST' },
189
+ 'mcpGetMilestone': { route: 'mcp/v1/milestones/by-id', method: 'GET' },
190
+ 'mcpUpdateMilestone': { route: 'mcp/v1/milestones', method: 'PUT' },
191
+ 'mcpListMilestones': { route: 'mcp/v1/milestones', method: 'GET' },
192
+ 'mcpDeleteMilestone': { route: 'mcp/v1/milestones', method: 'DELETE' },
193
+ // Epic-to-milestone linking
194
+ 'mcpLinkEpicToMilestone': { route: 'mcp/v1/milestones/link-epic', method: 'POST' },
195
+ 'mcpUnlinkEpicFromMilestone': { route: 'mcp/v1/milestones/unlink-epic', method: 'POST' },
196
+ 'mcpGenerateMilestoneChangelog': { route: 'mcp/v1/milestones/generate-changelog', method: 'POST' },
197
+ 'mcpGetMilestoneProgress': { route: 'mcp/v1/milestones/progress', method: 'GET' },
198
+ 'mcpReorderMilestoneEpics': { route: 'mcp/v1/milestones/reorder-epics', method: 'POST' },
199
+ // Suite-to-milestone linking
200
+ 'mcpLinkSuiteToMilestone': { route: 'mcp/v1/milestones/link-suite', method: 'POST' },
201
+ 'mcpUnlinkSuiteFromMilestone': { route: 'mcp/v1/milestones/unlink-suite', method: 'POST' },
202
+
203
+ // Features (Feature Compendium, E-162) — org-level product capabilities
204
+ 'mcpCreateFeature': { route: 'mcp/v1/features', method: 'POST' },
205
+ 'mcpGetFeature': { route: 'mcp/v1/features/by-id', method: 'GET' },
206
+ 'mcpListFeatures': { route: 'mcp/v1/features', method: 'GET' },
207
+ 'mcpUpdateFeature': { route: 'mcp/v1/features', method: 'PUT' },
208
+ 'mcpDeleteFeature': { route: 'mcp/v1/features', method: 'DELETE' },
209
+ 'mcpListFeatureLinks': { route: 'mcp/v1/features/links', method: 'GET' },
210
+ 'mcpGetFeatureDetail': { route: 'mcp/v1/features/detail', method: 'GET' },
211
+ 'mcpSearchFeatures': { route: 'mcp/v1/features/search', method: 'GET' },
212
+ 'mcpLinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'POST' },
213
+ 'mcpUnlinkFeatureArtifact': { route: 'mcp/v1/features/link', method: 'DELETE' },
214
+ 'mcpPromoteEpicToFeature': { route: 'mcp/v1/features/promote-epic', method: 'POST' },
215
+ 'mcpGenerateHowItWorks': { route: 'mcp/v1/features/generate-how-it-works', method: 'POST' },
216
+ 'mcpApplyHowItWorks': { route: 'mcp/v1/features/apply-how-it-works', method: 'POST' },
217
+ 'mcpApplyFeatureInit': { route: 'mcp/v1/features/init-apply', method: 'POST' },
218
+
219
+ // Decisions / ADRs (E-170) — org-level durable decision records that link to
220
+ // features and other artifacts via the generic link graph.
221
+ 'mcpCreateDecision': { route: 'mcp/v1/decisions', method: 'POST' },
222
+ 'mcpListDecisions': { route: 'mcp/v1/decisions', method: 'GET' },
223
+ 'mcpGetDecision': { route: 'mcp/v1/decisions/by-id', method: 'GET' },
224
+ 'mcpListDecisionLinks': { route: 'mcp/v1/decisions/links', method: 'GET' },
225
+ 'mcpUpdateDecision': { route: 'mcp/v1/decisions', method: 'PUT' },
226
+ 'mcpDeleteDecision': { route: 'mcp/v1/decisions', method: 'DELETE' },
227
+ 'mcpSupersedeDecision': { route: 'mcp/v1/decisions/supersede', method: 'POST' },
228
+ 'mcpPromoteDecisionFromKnowledge': { route: 'mcp/v1/decisions/promote-from-knowledge', method: 'POST' },
229
+ 'mcpLinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'POST' },
230
+ 'mcpUnlinkDecisionArtifact': { route: 'mcp/v1/decisions/link', method: 'DELETE' },
231
+
232
+ // Recurring task schedules (E-211) — "what task to create, on what cadence".
233
+ 'mcpCreateRecurringTask': { route: 'mcp/v1/recurring-tasks', method: 'POST' },
234
+ 'mcpListRecurringTasks': { route: 'mcp/v1/recurring-tasks', method: 'GET' },
235
+ 'mcpGetRecurringTask': { route: 'mcp/v1/recurring-tasks/by-id', method: 'GET' },
236
+ 'mcpUpdateRecurringTask': { route: 'mcp/v1/recurring-tasks', method: 'PUT' },
237
+ 'mcpPauseRecurringTask': { route: 'mcp/v1/recurring-tasks/pause', method: 'POST' },
238
+ 'mcpResumeRecurringTask': { route: 'mcp/v1/recurring-tasks/resume', method: 'POST' },
239
+ 'mcpDeleteRecurringTask': { route: 'mcp/v1/recurring-tasks', method: 'DELETE' },
240
+
241
+ // Work templates (E-211) — reusable task/epic blueprints + instantiate.
242
+ 'mcpCreateWorkTemplate': { route: 'mcp/v1/work-templates', method: 'POST' },
243
+ 'mcpListWorkTemplates': { route: 'mcp/v1/work-templates', method: 'GET' },
244
+ 'mcpGetWorkTemplate': { route: 'mcp/v1/work-templates/by-id', method: 'GET' },
245
+ 'mcpUpdateWorkTemplate': { route: 'mcp/v1/work-templates', method: 'PUT' },
246
+ 'mcpDeleteWorkTemplate': { route: 'mcp/v1/work-templates', method: 'DELETE' },
247
+ 'mcpInstantiateWorkTemplate': { route: 'mcp/v1/work-templates/instantiate', method: 'POST' },
248
+
249
+ // Designs (the in-product living design system) — org-level AI-authored HTML/CSS
250
+ // artifacts (kind theme|component|page) that link to features and other artifacts.
251
+ 'mcpCreateDesign': { route: 'mcp/v1/designs', method: 'POST' },
252
+ 'mcpListDesigns': { route: 'mcp/v1/designs', method: 'GET' },
253
+ 'mcpGetDesign': { route: 'mcp/v1/designs/by-id', method: 'GET' },
254
+ 'mcpGetDesignSystem': { route: 'mcp/v1/designs/system', method: 'GET' },
255
+ 'mcpListDesignLinks': { route: 'mcp/v1/designs/links', method: 'GET' },
256
+ 'mcpUpdateDesign': { route: 'mcp/v1/designs', method: 'PUT' },
257
+ 'mcpDeleteDesign': { route: 'mcp/v1/designs', method: 'DELETE' },
258
+ 'mcpLinkDesign': { route: 'mcp/v1/designs/link', method: 'POST' },
259
+ 'mcpUnlinkDesign': { route: 'mcp/v1/designs/link', method: 'DELETE' },
260
+
261
+ // Catalogs (E-215) — org-level generalized code-derived catalogs, each versioned
262
+ 'mcpCreateCatalog': { route: 'mcp/v1/catalogs', method: 'POST' },
263
+ 'mcpListCatalogs': { route: 'mcp/v1/catalogs', method: 'GET' },
264
+ 'mcpGetCatalog': { route: 'mcp/v1/catalogs/by-id', method: 'GET' },
265
+ 'mcpGetCurrentCatalog': { route: 'mcp/v1/catalogs/current', method: 'GET' },
266
+ 'mcpListCatalogVersions': { route: 'mcp/v1/catalogs/versions', method: 'GET' },
267
+ 'mcpGetCatalogVersion': { route: 'mcp/v1/catalogs/version', method: 'GET' },
268
+ 'mcpDiffCatalog': { route: 'mcp/v1/catalogs/diff', method: 'GET' },
269
+ 'mcpListCatalogLinks': { route: 'mcp/v1/catalogs/links', method: 'GET' },
270
+ 'mcpUpdateCatalog': { route: 'mcp/v1/catalogs', method: 'PUT' },
271
+ 'mcpDeleteCatalog': { route: 'mcp/v1/catalogs', method: 'DELETE' },
272
+ 'mcpSnapshotCatalog': { route: 'mcp/v1/catalogs/snapshot', method: 'POST' },
273
+ 'mcpLinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'POST' },
274
+ 'mcpUnlinkCatalog': { route: 'mcp/v1/catalogs/link', method: 'DELETE' },
275
+ // Item-level links (E-218) — attach work or an external URL to one catalog entry
276
+ 'mcpListCatalogItems': { route: 'mcp/v1/catalogs/items', method: 'GET' },
277
+ 'mcpLinkCatalogItem': { route: 'mcp/v1/catalogs/items/link', method: 'POST' },
278
+ 'mcpUnlinkCatalogItem': { route: 'mcp/v1/catalogs/items/link', method: 'DELETE' },
279
+
280
+ // Feature Flags (E-149) — org-level PM-aware runtime gates that link to the
281
+ // feature(s) they gate. /evaluate runs the deterministic, prerequisite-aware
282
+ // evaluator through the in-process cache.
283
+ 'mcpCreateFeatureFlag': { route: 'mcp/v1/feature-flags', method: 'POST' },
284
+ 'mcpListFeatureFlags': { route: 'mcp/v1/feature-flags', method: 'GET' },
285
+ 'mcpGetFeatureFlag': { route: 'mcp/v1/feature-flags/by-id', method: 'GET' },
286
+ 'mcpListFeatureFlagLinks': { route: 'mcp/v1/feature-flags/links', method: 'GET' },
287
+ 'mcpEvaluateFeatureFlag': { route: 'mcp/v1/feature-flags/evaluate', method: 'POST' },
288
+ 'mcpUpdateFeatureFlag': { route: 'mcp/v1/feature-flags', method: 'PUT' },
289
+ 'mcpDeleteFeatureFlag': { route: 'mcp/v1/feature-flags', method: 'DELETE' },
290
+ 'mcpTransitionFeatureFlag': { route: 'mcp/v1/feature-flags/transition', method: 'POST' },
291
+ 'mcpLinkFeatureFlag': { route: 'mcp/v1/feature-flags/link', method: 'POST' },
292
+ 'mcpUnlinkFeatureFlag': { route: 'mcp/v1/feature-flags/link', method: 'DELETE' },
293
+ // Environment registry + per-env flag config (E-186).
294
+ 'mcpListEnvironments': { route: 'mcp/v1/feature-flags/environments', method: 'GET' },
295
+ 'mcpCreateEnvironment': { route: 'mcp/v1/feature-flags/environments', method: 'POST' },
296
+ 'mcpUpdateEnvironment': { route: 'mcp/v1/feature-flags/environments', method: 'PUT' },
297
+ 'mcpDeleteEnvironment': { route: 'mcp/v1/feature-flags/environments', method: 'DELETE' },
298
+ 'mcpSetDefaultEnvironment': { route: 'mcp/v1/feature-flags/environments/default', method: 'POST' },
299
+ 'mcpSetFlagEnvironmentConfig': { route: 'mcp/v1/feature-flags/flag-environment', method: 'PUT' },
300
+
301
+ // Activity Timeline
302
+ 'mcpGetProjectChanges': { route: 'mcp/v1/projects/activity', method: 'GET' },
303
+
304
+ // Context Manifest (cloud-backed project context)
305
+ 'mcpSearchManifest': { route: 'mcp/v1/manifest/search', method: 'POST' },
306
+ 'mcpGetRelatedFiles': { route: 'mcp/v1/manifest/related', method: 'GET' },
307
+ 'mcpGetManifestOverview': { route: 'mcp/v1/manifest/overview', method: 'GET' },
308
+ 'mcpGetCriticalFiles': { route: 'mcp/v1/manifest/critical-files', method: 'GET' },
309
+ 'mcpUpdateManifestEntries': { route: 'mcp/v1/manifest/update-entries', method: 'POST' },
310
+ 'mcpRebuildManifest': { route: 'mcp/v1/manifest/rebuild', method: 'POST' },
311
+
312
+ // Knowledge Graph (graph query tools)
313
+ 'mcpQueryProjectGraph': { route: 'mcp/v1/graph/query', method: 'POST' },
314
+ 'mcpAnalyzeImpact': { route: 'mcp/v1/graph/impact', method: 'POST' },
315
+ 'mcpGetGraphStats': { route: 'mcp/v1/graph/stats', method: 'GET' },
316
+ // Navigation-graph projection over entity_links (E-209) — agent traversal
317
+ 'mcpTraverseGraph': { route: 'mcp/v1/graph/traverse', method: 'POST' },
318
+
319
+ // GitHub endpoints
320
+ 'mcpCreatePullRequest': { route: 'mcp/v1/github/pull-request', method: 'POST' },
321
+ 'mcpUpdatePullRequest': { route: 'mcp/v1/github/pull-request', method: 'PUT' },
322
+ 'mcpMergePullRequest': { route: 'mcp/v1/github/pull-request/merge', method: 'POST' },
323
+ 'mcpClosePullRequest': { route: 'mcp/v1/github/pull-request/close', method: 'POST' },
324
+ 'mcpRequestPRReview': { route: 'mcp/v1/github/pull-request/review', method: 'POST' },
325
+ 'mcpSuggestReviewers': { route: 'mcp/v1/github/pull-request/suggest-reviewers', method: 'POST' },
326
+ 'mcpListRepositories': { route: 'mcp/v1/github/repositories', method: 'GET' },
327
+
328
+ // Todos (personal todo list)
329
+ 'mcpCreateTodo': { route: 'mcp/v1/todos', method: 'POST' },
330
+ 'mcpListTodos': { route: 'mcp/v1/todos', method: 'GET' },
331
+ 'mcpCompleteTodo': { route: 'mcp/v1/todos/complete', method: 'POST' },
332
+ 'mcpMoveTodo': { route: 'mcp/v1/todos/move', method: 'POST' },
333
+
334
+ // Testing (project test cases)
335
+ 'mcpListTestCases': { route: 'mcp/v1/testing/cases', method: 'GET' },
336
+ 'mcpGetTestCase': { route: 'mcp/v1/testing/case', method: 'GET' },
337
+ 'mcpCreateTestCase': { route: 'mcp/v1/testing/cases', method: 'POST' },
338
+ 'mcpUpdateTestCase': { route: 'mcp/v1/testing/cases', method: 'PUT' },
339
+ 'mcpDeleteTestCase': { route: 'mcp/v1/testing/cases', method: 'DELETE' },
340
+ 'mcpRecordTestRun': { route: 'mcp/v1/testing/runs', method: 'POST' },
341
+ 'mcpGetTestingSummary': { route: 'mcp/v1/testing/summary', method: 'GET' },
342
+
343
+ // Test Suites (project-level test case grouping)
344
+ 'mcpListTestSuites': { route: 'mcp/v1/testing/suites', method: 'GET' },
345
+ 'mcpGetTestSuite': { route: 'mcp/v1/testing/suites/by-id', method: 'GET' },
346
+ 'mcpCreateTestSuite': { route: 'mcp/v1/testing/suites', method: 'POST' },
347
+ 'mcpUpdateTestSuite': { route: 'mcp/v1/testing/suites', method: 'PUT' },
348
+ 'mcpDeleteTestSuite': { route: 'mcp/v1/testing/suites', method: 'DELETE' },
349
+ 'mcpAddCasesToSuite': { route: 'mcp/v1/testing/suites/add-cases', method: 'POST' },
350
+ 'mcpRemoveCasesFromSuite': { route: 'mcp/v1/testing/suites/remove-cases', method: 'POST' },
351
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Environment Configuration Loader
3
+ *
4
+ * Imports the appropriate config based on BUILD_ENV at build time.
5
+ * This file is used during the build process to bake in the environment.
6
+ *
7
+ * BUILD_ENV should be set when building:
8
+ * - BUILD_ENV=production (for production builds - the default)
9
+ * - BUILD_ENV=staging (for staging builds)
10
+ * - BUILD_ENV=development (for local dev)
11
+ *
12
+ * The default is PRODUCTION, not development. Every in-repo consumer sets
13
+ * BUILD_ENV explicitly -- `npm run dev`, the test scripts, and the CLI's
14
+ * embedded server (cli/src/lib/mcp-server.ts assigns process.env.BUILD_ENV
15
+ * from BUILD_ENVIRONMENT before importing a handler). So the ONLY caller that
16
+ * arrives here with it unset is an installed copy: `npx @ezmodo/mcp-server`,
17
+ * or the Claude Code plugin that launches it (E-252 #2593). Defaulting those
18
+ * to development pointed a real user's tools at http://localhost:8080/api and
19
+ * failed with a connection error that says nothing about why.
20
+ */
21
+
22
+ const BUILD_ENV = process.env.BUILD_ENV || 'production';
23
+
24
+ let config;
25
+
26
+ if (BUILD_ENV === 'production') {
27
+ config = await import('./production.js');
28
+ } else if (BUILD_ENV === 'staging') {
29
+ config = await import('./staging.js');
30
+ } else {
31
+ config = await import('./development.js');
32
+ }
33
+
34
+ export const CONFIG = config.CONFIG;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Production Environment Configuration
3
+ *
4
+ * This config is baked into the production build at compile time.
5
+ * No runtime environment variables needed (except EZMODO_API_KEY).
6
+ */
7
+
8
+ export const CONFIG = {
9
+ environment: 'production',
10
+ apiUrl: 'https://ezmodo.com/api',
11
+ webUrl: 'https://ezmodo.com',
12
+ settingsUrl: 'https://ezmodo.com/settings/api-keys',
13
+
14
+ // Environment URLs for reference/docs
15
+ environmentUrls: {
16
+ production: 'https://ezmodo.com/api',
17
+ }
18
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Staging Environment Configuration
3
+ *
4
+ * This config is baked into the staging build at compile time.
5
+ * No runtime environment variables needed (except EZMODO_API_KEY).
6
+ */
7
+
8
+ export const CONFIG = {
9
+ environment: 'staging',
10
+ apiUrl: 'https://staging.ezmodo.com/api',
11
+ webUrl: 'https://staging.ezmodo.com',
12
+ settingsUrl: 'https://staging.ezmodo.com/settings/api-keys',
13
+
14
+ // Environment URLs for reference/docs
15
+ environmentUrls: {
16
+ staging: 'https://staging.ezmodo.com/api',
17
+ }
18
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Entity Access Handlers (#2170)
3
+ *
4
+ * Thin wrappers over /mcp/v1/access — the polymorphic entity-access surface.
5
+ * All authorization happens server-side: the Go handlers share one
6
+ * implementation with the REST routes (api/internal/api/handlers/access_ops.go),
7
+ * so the permission thresholds cannot differ between the two transports and
8
+ * there is nothing to enforce here.
9
+ *
10
+ * What this layer DOES owe the caller is honesty about parameters. Every field
11
+ * below is forwarded to an endpoint that uses it, and a field that belongs to a
12
+ * different action is rejected rather than dropped — the failure mode this whole
13
+ * line of work exists to eliminate is a parameter that is advertised, accepted,
14
+ * and silently discarded (#2168, #2169, #2171).
15
+ */
16
+
17
+ import { callZephlyAPI } from '../lib/http-client.js';
18
+
19
+ // Fields that only mean something for one action. Passing one to a different
20
+ // action is a mistake worth surfacing: silently ignoring `role` on a
21
+ // remove_entry call would look exactly like a successful role change.
22
+ const ACTION_ONLY_FIELDS = {
23
+ update_settings: ['inheritFromParent', 'accessList', 'publicAccess'],
24
+ add_entry: ['userId', 'teamId', 'role'],
25
+ remove_entry: ['entryId'],
26
+ check: ['requiredRole'],
27
+ };
28
+
29
+ const ALL_ACTIONS = Object.keys(ACTION_ONLY_FIELDS);
30
+
31
+ /**
32
+ * Reject fields belonging to another action instead of dropping them.
33
+ */
34
+ function rejectForeignFields(action, params) {
35
+ const allowed = new Set(ACTION_ONLY_FIELDS[action]);
36
+ const foreign = [];
37
+
38
+ for (const [otherAction, fields] of Object.entries(ACTION_ONLY_FIELDS)) {
39
+ if (otherAction === action) continue;
40
+ for (const field of fields) {
41
+ if (params[field] !== undefined && !allowed.has(field)) {
42
+ foreign.push(`${field} (belongs to action "${otherAction}")`);
43
+ }
44
+ }
45
+ }
46
+
47
+ if (foreign.length > 0) {
48
+ throw new Error(
49
+ `manage_access action "${action}" does not use: ${foreign.join(', ')}. ` +
50
+ 'Remove the field or use the action it belongs to — it would otherwise be ' +
51
+ 'accepted and discarded, which reads as a successful change.'
52
+ );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Read an entity's access settings. Requires viewer access to the entity.
58
+ */
59
+ export async function getAccess({ entityType, entityId, organizationId }) {
60
+ return callZephlyAPI('mcpGetAccess', { entityType, entityId, organizationId });
61
+ }
62
+
63
+ /**
64
+ * Dispatch manage_access to the appropriate operation.
65
+ */
66
+ export async function manageAccess(args) {
67
+ const { action, ...params } = args;
68
+
69
+ if (!ALL_ACTIONS.includes(action)) {
70
+ throw new Error(
71
+ `Unknown action: ${action}. Expected one of: ${ALL_ACTIONS.join(', ')}.`
72
+ );
73
+ }
74
+
75
+ rejectForeignFields(action, params);
76
+
77
+ switch (action) {
78
+ case 'update_settings': return updateAccessSettings(params);
79
+ case 'add_entry': return addAccessEntry(params);
80
+ case 'remove_entry': return removeAccessEntry(params);
81
+ case 'check': return checkAccess(params);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Replace an entity's access configuration. Requires admin on the entity and
87
+ * the write:access scope.
88
+ */
89
+ async function updateAccessSettings({
90
+ entityType, entityId, organizationId, inheritFromParent, accessList, publicAccess,
91
+ }) {
92
+ const body = { entityType, entityId, organizationId };
93
+ // Only forward what was actually set: these are tri-state server-side
94
+ // (unset means "leave alone"), so sending explicit nulls would clear them.
95
+ if (inheritFromParent !== undefined) body.inheritFromParent = inheritFromParent;
96
+ if (accessList !== undefined) body.accessList = accessList;
97
+ if (publicAccess !== undefined) body.publicAccess = publicAccess;
98
+
99
+ if (Object.keys(body).length === 3) {
100
+ throw new Error(
101
+ 'manage_access action "update_settings" needs at least one of ' +
102
+ 'inheritFromParent, accessList, or publicAccess.'
103
+ );
104
+ }
105
+
106
+ return callZephlyAPI('mcpUpdateAccess', body);
107
+ }
108
+
109
+ /**
110
+ * Grant one user or team a role. Requires admin on the entity and the
111
+ * write:access scope.
112
+ */
113
+ async function addAccessEntry({ entityType, entityId, organizationId, userId, teamId, role }) {
114
+ const body = { entityType, entityId, organizationId, role };
115
+ if (userId) body.userId = userId;
116
+ if (teamId) body.teamId = teamId;
117
+ return callZephlyAPI('mcpAddAccessEntry', body);
118
+ }
119
+
120
+ /**
121
+ * Revoke a single access entry. Requires admin on the entity and the
122
+ * write:access scope.
123
+ */
124
+ async function removeAccessEntry({ entityType, entityId, organizationId, entryId }) {
125
+ return callZephlyAPI('mcpRemoveAccessEntry', {
126
+ entityType,
127
+ entityId,
128
+ organizationId,
129
+ entryId,
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Report whether the calling key's user holds a role on the entity. A denial is
135
+ * a normal successful response carrying granted:false.
136
+ */
137
+ async function checkAccess({ entityType, entityId, organizationId, requiredRole }) {
138
+ const body = { entityType, entityId, organizationId };
139
+ if (requiredRole) body.requiredRole = requiredRole;
140
+ return callZephlyAPI('mcpCheckAccess', body);
141
+ }