@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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epic Handlers
|
|
3
|
+
* Handler functions for epic-related MCP tools
|
|
4
|
+
*
|
|
5
|
+
* Project-First Hierarchy: Epics belong to projects (required),
|
|
6
|
+
* with optional component grouping and milestone linking.
|
|
7
|
+
*
|
|
8
|
+
* Auto-assignment: When creating epics, automatically applies matching tags
|
|
9
|
+
* based on content analysis against the local project cache.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
13
|
+
import { resolveEpicAutoAssign } from '../lib/auto-assign.js';
|
|
14
|
+
import { buildEpicUrl } from '../lib/web-url.js';
|
|
15
|
+
import { getLogger } from '../lib/logger.js';
|
|
16
|
+
import { attachLinks } from '../lib/links-at-create.js';
|
|
17
|
+
import { normalizeChangedFiles } from '../lib/changed-files.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Apply tags to a newly created entity via bulkTagEntities.
|
|
21
|
+
* Non-fatal — logs errors but doesn't throw.
|
|
22
|
+
*/
|
|
23
|
+
async function applyAutoTags(organizationId, entityType, entityId, tagIds) {
|
|
24
|
+
if (!organizationId || !tagIds?.length || !entityId) return;
|
|
25
|
+
try {
|
|
26
|
+
await callZephlyAPI('mcpBulkTagEntities', {
|
|
27
|
+
organizationId,
|
|
28
|
+
tagIds,
|
|
29
|
+
entities: [{ entityType, entityId }],
|
|
30
|
+
operation: 'add',
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
getLogger().warn('Auto-tag failed', { entityType, entityId, error: err.message });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flatten the nested-task result into the fields an agent acts on (#2247).
|
|
39
|
+
*
|
|
40
|
+
* Mutates `result` in place, mirroring create_tasks: failures are surfaced as
|
|
41
|
+
* their own list because an agent scanning a 40-entry array can miss three error
|
|
42
|
+
* fields, and re-running the create is the costly mistake — it would make a
|
|
43
|
+
* second epic on top of duplicating the tasks that succeeded.
|
|
44
|
+
*/
|
|
45
|
+
function summarizeNestedTasks(result) {
|
|
46
|
+
if (!result) return;
|
|
47
|
+
const created = result.tasks?.created ?? 0;
|
|
48
|
+
const failed = result.tasks?.failed ?? 0;
|
|
49
|
+
const results = result.tasks?.results ?? [];
|
|
50
|
+
|
|
51
|
+
if (result.tasksError) {
|
|
52
|
+
result.message = `Epic created, but no tasks were: ${result.tasksError}. ` +
|
|
53
|
+
'The epic exists — send the tasks with create_tasks rather than creating the epic again.';
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
result.message = failed === 0
|
|
58
|
+
? `Created the epic and ${created} task${created === 1 ? '' : 's'} in one request.`
|
|
59
|
+
: `Epic created with ${created} of ${created + failed} tasks. ${failed} failed — the epic and the ` +
|
|
60
|
+
'successful tasks exist, so retry ONLY the items listed in failures with create_tasks.';
|
|
61
|
+
|
|
62
|
+
if (failed > 0) {
|
|
63
|
+
result.failures = results
|
|
64
|
+
.filter((r) => r.error)
|
|
65
|
+
.map((r) => ({ index: r.index, title: r.title, error: r.error }));
|
|
66
|
+
}
|
|
67
|
+
if (created > 0) {
|
|
68
|
+
result.taskNumbers = results.filter((r) => r.taskNumber).map((r) => r.taskNumber);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Dispatch manage_epic actions to the appropriate handler
|
|
74
|
+
*/
|
|
75
|
+
export async function manageEpic(args) {
|
|
76
|
+
const { action, ...params } = args;
|
|
77
|
+
switch (action) {
|
|
78
|
+
case 'create': return createEpic(params);
|
|
79
|
+
case 'update': return updateEpic(params);
|
|
80
|
+
case 'generate_how_it_works': return generateEpicHowItWorks(params);
|
|
81
|
+
case 'apply_how_it_works': return applyEpicHowItWorks(params);
|
|
82
|
+
default: throw new Error(
|
|
83
|
+
`Unknown action: ${action}. Expected create, update, generate_how_it_works, or apply_how_it_works.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// (Re)generate the epic's grounded, source-attributed "how it works" living
|
|
89
|
+
// description (E-237 #2382). An epic's reality is its tasks — their status,
|
|
90
|
+
// captured decisions and linked commits — and its intent is the epic description
|
|
91
|
+
// plus the decisions reached by walking those tasks (settled model A, #2376).
|
|
92
|
+
// Server-side: one model call, AI-quota gated.
|
|
93
|
+
async function generateEpicHowItWorks({ epicId }) {
|
|
94
|
+
return callZephlyAPI('mcpGenerateEpicHowItWorks', { epicId });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Apply (persist) a LOCAL-agent-authored "how it works" for an epic (BYO-AI,
|
|
98
|
+
// E-190). Cited sources are validated server-side against the real grounded
|
|
99
|
+
// context before saving; no model call, so no AI quota is spent.
|
|
100
|
+
async function applyEpicHowItWorks({ epicId, markdown, sources }) {
|
|
101
|
+
return callZephlyAPI('mcpApplyEpicHowItWorks', { epicId, markdown, sources });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function createEpic(args) {
|
|
105
|
+
// `links` is applied by the MCP layer after the epic exists (E-225).
|
|
106
|
+
const { links, tasks, ...createArgs } = args;
|
|
107
|
+
const { title, description } = createArgs;
|
|
108
|
+
|
|
109
|
+
// Checked client-side too so an oversized breakdown fails immediately with
|
|
110
|
+
// actionable advice rather than spending a request to be rejected.
|
|
111
|
+
if (tasks?.length > 40) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`cannot create ${tasks.length} tasks with an epic in one call (limit 40) — ` +
|
|
114
|
+
'create the epic with the first 40, then send the rest with create_tasks',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// An epic with its breakdown goes to the composing endpoint (#2247) so the
|
|
119
|
+
// whole thing is one request; without tasks nothing changes. Everything below
|
|
120
|
+
// this line — auto-tags, links, web URL — acts on the EPIC and so is identical
|
|
121
|
+
// either way.
|
|
122
|
+
const result = tasks?.length
|
|
123
|
+
? await callZephlyAPI('mcpCreateEpicWithTasks', {
|
|
124
|
+
...createArgs,
|
|
125
|
+
tasks: tasks.map(({ changedFiles, linkedFiles, ...rest }) => ({
|
|
126
|
+
...rest,
|
|
127
|
+
// Accept the same two spellings as manage_task, matching create_tasks.
|
|
128
|
+
linkedFiles: normalizeChangedFiles(changedFiles, linkedFiles),
|
|
129
|
+
})),
|
|
130
|
+
})
|
|
131
|
+
: await callZephlyAPI('mcpCreateEpic', createArgs);
|
|
132
|
+
|
|
133
|
+
if (tasks?.length) summarizeNestedTasks(result);
|
|
134
|
+
|
|
135
|
+
// Auto-apply matching tags (non-fatal)
|
|
136
|
+
const autoAssign = await resolveEpicAutoAssign(title, description);
|
|
137
|
+
if (autoAssign?.matchedTags?.length && result?.epicId) {
|
|
138
|
+
await applyAutoTags(
|
|
139
|
+
autoAssign.organizationId,
|
|
140
|
+
'epic',
|
|
141
|
+
result.epicId,
|
|
142
|
+
autoAssign.matchedTags.map((t) => t.id),
|
|
143
|
+
);
|
|
144
|
+
result.autoAssigned = {
|
|
145
|
+
tags: autoAssign.matchedTags.map((t) => t.name),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Attach create-time links (E-225) — best effort, never fails the create.
|
|
150
|
+
await attachLinks(result, { sourceType: 'epic', sourceId: result?.epicId, links });
|
|
151
|
+
|
|
152
|
+
// Enrich with web URL
|
|
153
|
+
const webUrl = await buildEpicUrl(result?.epicNumber);
|
|
154
|
+
if (webUrl) result.webUrl = webUrl;
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function updateEpic(args) {
|
|
160
|
+
const result = await callZephlyAPI('mcpUpdateEpic', args);
|
|
161
|
+
|
|
162
|
+
// If the milestone is frozen and the operation was blocked, return guidance
|
|
163
|
+
if (result?.blocked) {
|
|
164
|
+
return {
|
|
165
|
+
blocked: true,
|
|
166
|
+
freezeType: result.freezeType,
|
|
167
|
+
milestoneId: result.milestoneId,
|
|
168
|
+
milestoneName: result.milestoneName,
|
|
169
|
+
message: `Milestone Frozen (${result.milestoneName || 'unknown'}, ${result.freezeType}): ${result.reason || 'No reason provided'}`,
|
|
170
|
+
suggestion: result.suggestion,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function searchEpics(args) {
|
|
178
|
+
return callZephlyAPI('mcpSearchEpics', args);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function listEpics(args) {
|
|
182
|
+
return callZephlyAPI('mcpListEpics', args);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function getEpic(args) {
|
|
186
|
+
const result = await callZephlyAPI('mcpGetEpic', args);
|
|
187
|
+
const webUrl = await buildEpicUrl(result?.epic?.epicNumber);
|
|
188
|
+
if (webUrl && result?.epic) result.epic.webUrl = webUrl;
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fact Handlers
|
|
3
|
+
* Handler functions for fact-related MCP tools
|
|
4
|
+
*
|
|
5
|
+
* Facts are project-scoped knowledge entries for tracking:
|
|
6
|
+
* - Decisions and conventions
|
|
7
|
+
* - Reference information
|
|
8
|
+
* - Key context for humans and AI agents
|
|
9
|
+
*
|
|
10
|
+
* Write operations (create, update, delete) invalidate the cache.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
14
|
+
import { invalidateCacheSection } from '../lib/local-cache.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dispatch manage_fact actions to the appropriate handler
|
|
18
|
+
*/
|
|
19
|
+
export async function manageFact(args) {
|
|
20
|
+
const { action, ...params } = args;
|
|
21
|
+
switch (action) {
|
|
22
|
+
case 'create': return createFact(params);
|
|
23
|
+
case 'update': return updateFact(params);
|
|
24
|
+
case 'delete': return deleteFact(params);
|
|
25
|
+
default: throw new Error(`Unknown action: ${action}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List facts for a project, optionally retrieving a single fact by ID.
|
|
31
|
+
*/
|
|
32
|
+
export async function listFacts(args) {
|
|
33
|
+
const { projectId, factId } = args;
|
|
34
|
+
return callZephlyAPI('mcpListFacts', { projectId, factId });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a new fact
|
|
39
|
+
*/
|
|
40
|
+
async function createFact(args) {
|
|
41
|
+
const result = await callZephlyAPI('mcpCreateFact', args);
|
|
42
|
+
await invalidateCacheSection('facts');
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Update an existing fact
|
|
48
|
+
*/
|
|
49
|
+
async function updateFact(args) {
|
|
50
|
+
const result = await callZephlyAPI('mcpUpdateFact', args);
|
|
51
|
+
await invalidateCacheSection('facts');
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Delete a fact
|
|
57
|
+
*/
|
|
58
|
+
async function deleteFact(args) {
|
|
59
|
+
const result = await callZephlyAPI('mcpDeleteFact', args);
|
|
60
|
+
await invalidateCacheSection('facts');
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Flag Handlers
|
|
3
|
+
* Handler functions for the Feature Flags MCP tools (E-149).
|
|
4
|
+
*
|
|
5
|
+
* Feature Flags are org-level (projectId nullable) PM-aware runtime gates that
|
|
6
|
+
* LINK to the feature(s) they gate via the generic link graph. These handlers are
|
|
7
|
+
* thin wrappers over /api/mcp/v1/feature-flags; dispatch, validation, prerequisite
|
|
8
|
+
* resolution, and evaluation happen server-side in core/featureflags.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dispatch manage_feature_flag actions.
|
|
15
|
+
*/
|
|
16
|
+
export async function manageFeatureFlag(args) {
|
|
17
|
+
const { action, ...params } = args;
|
|
18
|
+
switch (action) {
|
|
19
|
+
case 'create': return createFeatureFlag(params);
|
|
20
|
+
case 'update': return updateFeatureFlag(params);
|
|
21
|
+
case 'delete': return deleteFeatureFlag(params);
|
|
22
|
+
case 'transition': return transitionFeatureFlag(params);
|
|
23
|
+
case 'link': return linkFeatureFlagArtifact(params);
|
|
24
|
+
case 'unlink': return unlinkFeatureFlagArtifact(params);
|
|
25
|
+
case 'set_environment_config': return setFeatureFlagEnvironmentConfig(params);
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Unknown action: ${action}. Expected create, update, delete, transition, link, unlink, ` +
|
|
29
|
+
'or set_environment_config.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dispatch manage_environment actions (E-186 environment registry).
|
|
35
|
+
*/
|
|
36
|
+
export async function manageEnvironment(args) {
|
|
37
|
+
const { action, ...params } = args;
|
|
38
|
+
switch (action) {
|
|
39
|
+
case 'list': return listEnvironments(params);
|
|
40
|
+
case 'create': return callZephlyAPI('mcpCreateEnvironment', params);
|
|
41
|
+
case 'update': return callZephlyAPI('mcpUpdateEnvironment', params);
|
|
42
|
+
case 'delete': return callZephlyAPI('mcpDeleteEnvironment', { environmentId: params.environmentId });
|
|
43
|
+
case 'set_default': return callZephlyAPI('mcpSetDefaultEnvironment', { environmentId: params.environmentId });
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unknown action: ${action}. Expected list, create, update, delete, or set_default.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function listEnvironments({ organizationId, projectId }) {
|
|
50
|
+
const params = { organizationId };
|
|
51
|
+
if (projectId) params.projectId = projectId;
|
|
52
|
+
return callZephlyAPI('mcpListEnvironments', params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function setFeatureFlagEnvironmentConfig({ flagId, environmentId, enabled, rolloutPercentage, defaultVariantId }) {
|
|
56
|
+
const body = { flagId, environmentId, enabled, rolloutPercentage };
|
|
57
|
+
if (defaultVariantId !== undefined) body.defaultVariantId = defaultVariantId;
|
|
58
|
+
return callZephlyAPI('mcpSetFlagEnvironmentConfig', body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve a single flag by ID or by organizationId + key (optionally + links).
|
|
63
|
+
*/
|
|
64
|
+
export async function getFeatureFlag(args) {
|
|
65
|
+
const { includeLinks, flagId, organizationId, key } = args;
|
|
66
|
+
|
|
67
|
+
const params = {};
|
|
68
|
+
if (flagId) {
|
|
69
|
+
params.flagId = flagId;
|
|
70
|
+
} else if (organizationId && key) {
|
|
71
|
+
params.organizationId = organizationId;
|
|
72
|
+
params.key = key;
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error('Provide flagId, or organizationId and key.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await callZephlyAPI('mcpGetFeatureFlag', params);
|
|
78
|
+
if (includeLinks) {
|
|
79
|
+
const id = flagId || result?.featureFlag?.id || result?.id;
|
|
80
|
+
if (id) {
|
|
81
|
+
const links = await callZephlyAPI('mcpListFeatureFlagLinks', { flagId: id });
|
|
82
|
+
result.links = links?.links ?? links;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List an org's flags (filtered), or the flags linked to an entity.
|
|
90
|
+
*/
|
|
91
|
+
export async function listFeatureFlags(args) {
|
|
92
|
+
const params = {};
|
|
93
|
+
if (args.organizationId) params.organizationId = args.organizationId;
|
|
94
|
+
if (args.linkedType && args.linkedId) {
|
|
95
|
+
params.linkedType = args.linkedType;
|
|
96
|
+
params.linkedId = args.linkedId;
|
|
97
|
+
return callZephlyAPI('mcpListFeatureFlags', params);
|
|
98
|
+
}
|
|
99
|
+
if (args.projectId) params.projectId = args.projectId;
|
|
100
|
+
if (args.status) params.status = args.status;
|
|
101
|
+
if (args.kind) params.kind = args.kind;
|
|
102
|
+
if (args.includeOrgWide != null) params.includeOrgWide = args.includeOrgWide;
|
|
103
|
+
if (args.limit != null) params.limit = args.limit;
|
|
104
|
+
return callZephlyAPI('mcpListFeatureFlags', params);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluate a flag for a subject — debugging affordance from chat.
|
|
109
|
+
*/
|
|
110
|
+
export async function evaluateFeatureFlag(args) {
|
|
111
|
+
const params = { organizationId: args.organizationId, key: args.key };
|
|
112
|
+
if (args.subjectId) params.subjectId = args.subjectId;
|
|
113
|
+
if (args.environment) params.environment = args.environment;
|
|
114
|
+
if (args.attributes) params.attributes = args.attributes;
|
|
115
|
+
return callZephlyAPI('mcpEvaluateFeatureFlag', params);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Private helpers ---
|
|
119
|
+
|
|
120
|
+
async function createFeatureFlag(args) {
|
|
121
|
+
return callZephlyAPI('mcpCreateFeatureFlag', args);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function updateFeatureFlag(args) {
|
|
125
|
+
return callZephlyAPI('mcpUpdateFeatureFlag', args);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function deleteFeatureFlag({ flagId }) {
|
|
129
|
+
return callZephlyAPI('mcpDeleteFeatureFlag', { flagId });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function transitionFeatureFlag({ flagId, status, reason }) {
|
|
133
|
+
return callZephlyAPI('mcpTransitionFeatureFlag', { flagId, status, reason });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function linkFeatureFlagArtifact({ flagId, targetType, targetId }) {
|
|
137
|
+
return callZephlyAPI('mcpLinkFeatureFlag', { flagId, targetType, targetId });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function unlinkFeatureFlagArtifact({ flagId, targetType, targetId }) {
|
|
141
|
+
return callZephlyAPI('mcpUnlinkFeatureFlag', { flagId, targetType, targetId });
|
|
142
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Handlers
|
|
3
|
+
* Handler functions for the Feature Compendium MCP tools (E-162).
|
|
4
|
+
*
|
|
5
|
+
* Features are org-level, durable product capabilities. These handlers are thin
|
|
6
|
+
* wrappers over /api/mcp/v1/features; dispatch and validation happen server-side
|
|
7
|
+
* in core/features.Service.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dispatch manage_feature actions.
|
|
14
|
+
*/
|
|
15
|
+
export async function manageFeature(args) {
|
|
16
|
+
const { action, ...params } = args;
|
|
17
|
+
switch (action) {
|
|
18
|
+
case 'create': return createFeature(params);
|
|
19
|
+
case 'update': return updateFeature(params);
|
|
20
|
+
case 'delete': return deleteFeature(params);
|
|
21
|
+
case 'link': return linkFeatureArtifact(params);
|
|
22
|
+
case 'unlink': return unlinkFeatureArtifact(params);
|
|
23
|
+
case 'promote_epic': return promoteEpic(params);
|
|
24
|
+
case 'generate_how_it_works': return generateHowItWorks(params);
|
|
25
|
+
case 'apply_how_it_works': return applyHowItWorks(params);
|
|
26
|
+
case 'apply_init': return applyInit(params);
|
|
27
|
+
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
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unified get/list/tree handler for features.
|
|
34
|
+
* - featureId, or organizationId + featureSlug → single lookup (optionally + links).
|
|
35
|
+
* - organizationId only → list (or tree when tree=true).
|
|
36
|
+
*/
|
|
37
|
+
export async function getFeature(args) {
|
|
38
|
+
const { tree, includeLinks, includeDetail, featureId, featureSlug, organizationId, ...filters } = args;
|
|
39
|
+
const isSingleLookup = featureId || (featureSlug && organizationId);
|
|
40
|
+
|
|
41
|
+
if (isSingleLookup) {
|
|
42
|
+
const params = featureId ? { featureId } : { organizationId, featureSlug };
|
|
43
|
+
const result = await callZephlyAPI('mcpGetFeature', params);
|
|
44
|
+
const lookupId = featureId || result?.feature?.id;
|
|
45
|
+
if (includeDetail && lookupId) {
|
|
46
|
+
// Aggregated view: linked artifacts grouped by type + rollup.
|
|
47
|
+
const detail = await callZephlyAPI('mcpGetFeatureDetail', { featureId: lookupId });
|
|
48
|
+
result.detail = detail?.detail ?? detail;
|
|
49
|
+
} else if (includeLinks && lookupId) {
|
|
50
|
+
const links = await callZephlyAPI('mcpListFeatureLinks', { featureId: lookupId });
|
|
51
|
+
result.links = links?.links ?? links;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// List / tree mode
|
|
57
|
+
const params = { organizationId };
|
|
58
|
+
// projectIds is the multi-project scope (E-242); the API takes it
|
|
59
|
+
// comma-separated and treats it as satisfying the mandatory project scope.
|
|
60
|
+
if (Array.isArray(filters.projectIds) && filters.projectIds.length > 0) {
|
|
61
|
+
params.projectIds = filters.projectIds.join(',');
|
|
62
|
+
} else if (filters.projectId) {
|
|
63
|
+
params.projectId = filters.projectId;
|
|
64
|
+
}
|
|
65
|
+
if (filters.status) params.status = filters.status;
|
|
66
|
+
if (tree) {
|
|
67
|
+
// A forest is a whole structure; the API ignores paging for it.
|
|
68
|
+
params.tree = 'true';
|
|
69
|
+
return callZephlyAPI('mcpListFeatures', params);
|
|
70
|
+
}
|
|
71
|
+
if (filters.search) params.search = filters.search;
|
|
72
|
+
if (filters.sortBy) params.sortBy = filters.sortBy;
|
|
73
|
+
if (filters.sortDir) params.sortDir = filters.sortDir;
|
|
74
|
+
if (filters.limit != null) params.limit = filters.limit;
|
|
75
|
+
if (filters.offset != null) params.offset = filters.offset;
|
|
76
|
+
return callZephlyAPI('mcpListFeatures', params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Semantic search over an organization's features (capture-at-build gate, E-164).
|
|
81
|
+
* Returns existing features ranked by similarity so callers add to an existing
|
|
82
|
+
* capability instead of creating a duplicate.
|
|
83
|
+
*/
|
|
84
|
+
export async function searchFeatures(args) {
|
|
85
|
+
const { organizationId, query, limit } = args;
|
|
86
|
+
const params = { organizationId, query };
|
|
87
|
+
if (limit != null) params.limit = limit;
|
|
88
|
+
return callZephlyAPI('mcpSearchFeatures', params);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Private helpers ---
|
|
92
|
+
|
|
93
|
+
async function createFeature(args) {
|
|
94
|
+
return callZephlyAPI('mcpCreateFeature', args);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function updateFeature(args) {
|
|
98
|
+
return callZephlyAPI('mcpUpdateFeature', args);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function deleteFeature({ featureId }) {
|
|
102
|
+
return callZephlyAPI('mcpDeleteFeature', { featureId });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function linkFeatureArtifact({ featureId, targetType, targetId }) {
|
|
106
|
+
return callZephlyAPI('mcpLinkFeatureArtifact', { featureId, targetType, targetId });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function unlinkFeatureArtifact({ featureId, targetType, targetId }) {
|
|
110
|
+
return callZephlyAPI('mcpUnlinkFeatureArtifact', { featureId, targetType, targetId });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function promoteEpic({ epicId }) {
|
|
114
|
+
return callZephlyAPI('mcpPromoteEpicToFeature', { epicId });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Generate (and persist) the feature's grounded "how it works" living
|
|
118
|
+
// description via the model (E-165). AI-quota gated server-side.
|
|
119
|
+
async function generateHowItWorks({ featureId }) {
|
|
120
|
+
return callZephlyAPI('mcpGenerateHowItWorks', { featureId });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply (persist) a LOCAL-agent-authored "how it works" for a feature (BYO-AI,
|
|
124
|
+
// E-190). The agent writes { markdown, sources }; the server validates the cited
|
|
125
|
+
// sources against the real grounded context before saving. No server model call.
|
|
126
|
+
async function applyHowItWorks({ featureId, markdown, sources }) {
|
|
127
|
+
return callZephlyAPI('mcpApplyHowItWorks', { featureId, markdown, sources });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Apply an approved init/backfill proposal tree (E-167): create features
|
|
131
|
+
// (parents→children), backfill links, and persist any agent-authored "how it
|
|
132
|
+
// works". The clustering/inference runs locally in the desktop with the user's
|
|
133
|
+
// own AI; this only commits the human-approved result. Server-side ApplyInit
|
|
134
|
+
// resolves tempId→featureId ordering and validates sources.
|
|
135
|
+
async function applyInit({ organizationId, projectId, nodes }) {
|
|
136
|
+
return callZephlyAPI('mcpApplyFeatureInit', { organizationId, projectId, nodes });
|
|
137
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder Handlers
|
|
3
|
+
* Handler functions for document folder-related MCP tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { callZephlyAPI } from '../lib/http-client.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dispatch manage_folder actions to the appropriate handler
|
|
10
|
+
*/
|
|
11
|
+
export async function manageFolder(args) {
|
|
12
|
+
const { action, ...params } = args;
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'create': return createFolder(params);
|
|
15
|
+
case 'update': return updateFolder(params);
|
|
16
|
+
case 'delete': return deleteFolder(params);
|
|
17
|
+
default: throw new Error(`Unknown action: ${action}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Unified list handler for folders.
|
|
23
|
+
* mode='tree' returns hierarchical view, mode='flat' (default) returns flat list.
|
|
24
|
+
*/
|
|
25
|
+
export async function listFolders(args) {
|
|
26
|
+
const { mode, ...rest } = args;
|
|
27
|
+
|
|
28
|
+
if (mode === 'tree') {
|
|
29
|
+
return getFolderTree(rest);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fetchFolderList(rest);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* List an organization's reserved document areas (Goals, Features, ADRs,
|
|
37
|
+
* How it works), creating any that are missing. (E-195)
|
|
38
|
+
* Params: organizationId (required).
|
|
39
|
+
*/
|
|
40
|
+
export async function getOrgAreas(args) {
|
|
41
|
+
return callZephlyAPI('mcpGetOrgAreas', args);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Private helpers ---
|
|
45
|
+
|
|
46
|
+
async function fetchFolderList(args) {
|
|
47
|
+
return callZephlyAPI('mcpListFolders', args);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getFolderTree(args) {
|
|
51
|
+
return callZephlyAPI('mcpGetFolderTree', args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Access is not a property of a folder write: neither folders.CreateFolderRequest
|
|
56
|
+
* nor UpdateFolderRequest carries an access field, so an `access` object was
|
|
57
|
+
* decoded away to nothing on both paths (#2168). The capability itself is real
|
|
58
|
+
* and now HAS an MCP surface of its own — the polymorphic manage_access tool
|
|
59
|
+
* (#2170) — so the honest response is to do the folder write and point at the
|
|
60
|
+
* tool that actually applies access, rather than dropping it silently or
|
|
61
|
+
* failing the whole call over it.
|
|
62
|
+
*/
|
|
63
|
+
const ACCESS_UNSUPPORTED =
|
|
64
|
+
'The `access` field was ignored: access is not set through manage_folder. ' +
|
|
65
|
+
'Use manage_access with entityType:"folder" (action "update_settings" or "add_entry") ' +
|
|
66
|
+
'to apply it. Note only root folders carry their own access — nested folders ' +
|
|
67
|
+
'inherit from their root folder.';
|
|
68
|
+
|
|
69
|
+
async function createFolder(args) {
|
|
70
|
+
// Map parentFolderId (MCP schema) → parentId (Go API field name)
|
|
71
|
+
const { parentFolderId, access, ...rest } = args;
|
|
72
|
+
const mapped = { ...rest };
|
|
73
|
+
if (parentFolderId) {
|
|
74
|
+
mapped.parentId = parentFolderId;
|
|
75
|
+
}
|
|
76
|
+
const result = await callZephlyAPI('mcpCreateFolder', mapped);
|
|
77
|
+
if (access !== undefined) result.warning = ACCESS_UNSUPPORTED;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function updateFolder(args) {
|
|
82
|
+
const { parentFolderId, access, ...rest } = args;
|
|
83
|
+
|
|
84
|
+
// Check if there are fields to update beyond projectId/folderId. `access` is
|
|
85
|
+
// deliberately NOT counted: it cannot be applied, so counting it would report
|
|
86
|
+
// a successful update that changed nothing.
|
|
87
|
+
const hasFieldUpdates = rest.name || rest.color || rest.icon;
|
|
88
|
+
let result;
|
|
89
|
+
|
|
90
|
+
if (hasFieldUpdates) {
|
|
91
|
+
result = await callZephlyAPI('mcpUpdateFolder', rest);
|
|
92
|
+
} else {
|
|
93
|
+
result = { success: true, folderId: args.folderId };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Move folder if parentFolderId was specified
|
|
97
|
+
if (parentFolderId !== undefined) {
|
|
98
|
+
const newParentId = (parentFolderId === '' || parentFolderId === 'root') ? null : parentFolderId;
|
|
99
|
+
await callZephlyAPI('mcpMoveFolder', {
|
|
100
|
+
projectId: args.projectId,
|
|
101
|
+
folderId: args.folderId,
|
|
102
|
+
newParentId,
|
|
103
|
+
});
|
|
104
|
+
result.moved = true;
|
|
105
|
+
result.newParentId = newParentId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!hasFieldUpdates && parentFolderId === undefined) {
|
|
109
|
+
// Name the access case explicitly. Reporting a bare "no updates provided"
|
|
110
|
+
// to a caller who did pass something reads as though the argument never
|
|
111
|
+
// arrived, sending them to debug their own call instead of the real answer.
|
|
112
|
+
throw new Error(
|
|
113
|
+
access !== undefined
|
|
114
|
+
? `No applicable updates provided. ${ACCESS_UNSUPPORTED} ` +
|
|
115
|
+
'To update the folder itself, pass one of: name, color, icon, or parentFolderId.'
|
|
116
|
+
: 'No updates provided. Specify at least one of: name, color, icon, or parentFolderId.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (access !== undefined) result.warning = ACCESS_UNSUPPORTED;
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function deleteFolder(args) {
|
|
126
|
+
return callZephlyAPI('mcpDeleteFolder', args);
|
|
127
|
+
}
|