@ezmodo/mcp-server 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/endpoint-map.js +6 -0
- package/handlers/epics.js +64 -76
- package/handlers/tasks.js +35 -31
- package/lib/auto-assign.js +8 -2
- package/lib/create-server.js +2 -1
- package/lib/suggested-tags.js +21 -0
- package/lib/tool-annotations.js +104 -0
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/tools/epics.js +50 -12
- package/tools/tasks.js +39 -2
package/config/endpoint-map.js
CHANGED
|
@@ -40,12 +40,18 @@ export const ENDPOINT_MAP = {
|
|
|
40
40
|
'mcpProposePlanChange': { route: 'mcp/v1/epics/proposals', method: 'POST' },
|
|
41
41
|
'mcpReviewPlanProposal': { route: 'mcp/v1/epics/proposals/review', method: 'POST' },
|
|
42
42
|
'mcpAddEpicComment': { route: 'mcp/v1/epics/comments', method: 'POST' },
|
|
43
|
+
// Editors (E-259 #2802): who the owner lets change the plan.
|
|
44
|
+
'mcpManageEpicEditors': { route: 'mcp/v1/epics/editors', method: 'POST' },
|
|
43
45
|
// E-237 #2382: the epic is the fifth consumer of the grounding engine.
|
|
44
46
|
'mcpGenerateEpicHowItWorks': { route: 'mcp/v1/epics/generate-how-it-works', method: 'POST' },
|
|
45
47
|
'mcpApplyEpicHowItWorks': { route: 'mcp/v1/epics/apply-how-it-works', method: 'POST' },
|
|
46
48
|
|
|
47
49
|
// Tasks
|
|
48
50
|
'mcpCreateTask': { route: 'mcp/v1/tasks', method: 'POST' },
|
|
51
|
+
// Claims (E-259 #2747): manage_task action "claim" / "release".
|
|
52
|
+
'mcpClaimTask': { route: 'mcp/v1/tasks/claim', method: 'POST' },
|
|
53
|
+
// Suggested edits on someone else's claimed task (E-259 #2809).
|
|
54
|
+
'mcpTaskSuggestedEdits': { route: 'mcp/v1/tasks/suggested-edits', method: 'POST' },
|
|
49
55
|
'mcpBulkCreateTasks': { route: 'mcp/v1/tasks/bulk', method: 'POST' },
|
|
50
56
|
'mcpUpdateTask': { route: 'mcp/v1/tasks', method: 'PUT' },
|
|
51
57
|
'mcpCompleteTask': { route: 'mcp/v1/tasks/complete', method: 'POST' },
|
package/handlers/epics.js
CHANGED
|
@@ -5,35 +5,18 @@
|
|
|
5
5
|
* Project-First Hierarchy: Epics belong to projects (required),
|
|
6
6
|
* with optional milestone linking.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Tags: only the tagIds the caller passes are applied, in the create request
|
|
9
|
+
* itself. Keyword matches against the local project cache come back as
|
|
10
|
+
* `suggestedTags` (see lib/auto-assign.js for why they are not applied).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { callZephlyAPI } from '../lib/http-client.js';
|
|
13
14
|
import { resolveEpicAutoAssign } from '../lib/auto-assign.js';
|
|
15
|
+
import { suggestTags } from '../lib/suggested-tags.js';
|
|
14
16
|
import { buildEpicUrl } from '../lib/web-url.js';
|
|
15
|
-
import { getLogger } from '../lib/logger.js';
|
|
16
17
|
import { attachLinks } from '../lib/links-at-create.js';
|
|
17
18
|
import { normalizeChangedFiles } from '../lib/changed-files.js';
|
|
18
19
|
|
|
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
20
|
/**
|
|
38
21
|
* Flatten the nested-task result into the fields an agent acts on (#2247).
|
|
39
22
|
*
|
|
@@ -79,12 +62,23 @@ export async function manageEpic(args) {
|
|
|
79
62
|
case 'update': return updateEpic(params);
|
|
80
63
|
case 'generate_how_it_works': return generateEpicHowItWorks(params);
|
|
81
64
|
case 'apply_how_it_works': return applyEpicHowItWorks(params);
|
|
65
|
+
case 'add_editor': return manageEpicEditor('add', params);
|
|
66
|
+
case 'remove_editor': return manageEpicEditor('remove', params);
|
|
82
67
|
default: throw new Error(
|
|
83
|
-
`Unknown action: ${action}. Expected create, update, generate_how_it_works,
|
|
68
|
+
`Unknown action: ${action}. Expected create, update, generate_how_it_works, apply_how_it_works, ` +
|
|
69
|
+
'add_editor or remove_editor.',
|
|
84
70
|
);
|
|
85
71
|
}
|
|
86
72
|
}
|
|
87
73
|
|
|
74
|
+
// Choose who else may change an epic's plan (E-259 #2802). The server decides
|
|
75
|
+
// who may do this; the tool only checks it was asked something answerable.
|
|
76
|
+
async function manageEpicEditor(action, { epicId, editorUserId }) {
|
|
77
|
+
if (!epicId) throw new Error(`epicId is required to ${action} an editor`);
|
|
78
|
+
if (!editorUserId) throw new Error(`editorUserId is required to ${action} an editor`);
|
|
79
|
+
return callZephlyAPI('mcpManageEpicEditors', { epicId, userId: editorUserId, action });
|
|
80
|
+
}
|
|
81
|
+
|
|
88
82
|
// (Re)generate the epic's grounded, source-attributed "how it works" living
|
|
89
83
|
// description (E-237 #2382). An epic's reality is its tasks — their status,
|
|
90
84
|
// captured decisions and linked commits — and its intent is the epic description
|
|
@@ -117,7 +111,7 @@ async function createEpic(args) {
|
|
|
117
111
|
|
|
118
112
|
// An epic with its breakdown goes to the composing endpoint (#2247) so the
|
|
119
113
|
// whole thing is one request; without tasks nothing changes. Everything below
|
|
120
|
-
// this line —
|
|
114
|
+
// this line — suggested tags, links, web URL — acts on the EPIC and so is identical
|
|
121
115
|
// either way.
|
|
122
116
|
const result = tasks?.length
|
|
123
117
|
? await callZephlyAPI('mcpCreateEpicWithTasks', {
|
|
@@ -132,18 +126,10 @@ async function createEpic(args) {
|
|
|
132
126
|
|
|
133
127
|
if (tasks?.length) summarizeNestedTasks(result);
|
|
134
128
|
|
|
135
|
-
//
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
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
|
-
};
|
|
129
|
+
// Suggest, never apply, keyword-matched tags (#2830).
|
|
130
|
+
const suggestedTags = suggestTags(await resolveEpicAutoAssign(title, description), createArgs.tagIds);
|
|
131
|
+
if (suggestedTags.length && result?.epicId) {
|
|
132
|
+
result.suggestedTags = suggestedTags;
|
|
147
133
|
}
|
|
148
134
|
|
|
149
135
|
// Attach create-time links (E-225) — best effort, never fails the create.
|
|
@@ -231,6 +217,8 @@ export async function getEpicActivity(args) {
|
|
|
231
217
|
const params = { epicId: args.epicId };
|
|
232
218
|
if (args.since) params.since = args.since;
|
|
233
219
|
if (args.markSeen === false) params.markSeen = 'false';
|
|
220
|
+
// Live mode (#2748): the API waits for news, up to its own cap.
|
|
221
|
+
if (args.waitSeconds > 0) params.waitSeconds = Math.min(Math.floor(args.waitSeconds), 25);
|
|
234
222
|
return callZephlyAPI('mcpGetEpicActivity', params);
|
|
235
223
|
}
|
|
236
224
|
|
|
@@ -258,53 +246,53 @@ export async function managePlanProposal(args = {}) {
|
|
|
258
246
|
const { action } = args;
|
|
259
247
|
|
|
260
248
|
switch (action) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return callZephlyAPI('mcpProposePlanChange', {
|
|
267
|
-
epicId: args.epicId,
|
|
268
|
-
plan: args.plan,
|
|
269
|
-
ops: args.ops,
|
|
270
|
-
title: args.title,
|
|
271
|
-
rationale: args.rationale,
|
|
272
|
-
});
|
|
249
|
+
case 'propose': {
|
|
250
|
+
if (!args.epicId) throw new Error('epicId is required to suggest a change');
|
|
251
|
+
if (!args.plan && !args.ops) {
|
|
252
|
+
throw new Error('Send the plan you want (or the individual changes) to suggest a change');
|
|
273
253
|
}
|
|
254
|
+
return callZephlyAPI('mcpProposePlanChange', {
|
|
255
|
+
epicId: args.epicId,
|
|
256
|
+
plan: args.plan,
|
|
257
|
+
ops: args.ops,
|
|
258
|
+
title: args.title,
|
|
259
|
+
rationale: args.rationale,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
274
262
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
263
|
+
case 'list': {
|
|
264
|
+
if (!args.epicId) throw new Error('epicId is required to list proposals');
|
|
265
|
+
const params = { epicId: args.epicId };
|
|
266
|
+
if (args.status) params.status = args.status;
|
|
267
|
+
return callZephlyAPI('mcpListPlanProposals', params);
|
|
268
|
+
}
|
|
281
269
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
270
|
+
case 'get': {
|
|
271
|
+
if (!args.proposalId) throw new Error('proposalId is required');
|
|
272
|
+
return callZephlyAPI('mcpListPlanProposals', { proposalId: args.proposalId });
|
|
273
|
+
}
|
|
286
274
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
return callZephlyAPI('mcpReviewPlanProposal', {
|
|
293
|
-
proposalId: args.proposalId,
|
|
294
|
-
accept: args.accept || [],
|
|
295
|
-
reject: args.reject || [],
|
|
296
|
-
note: args.note,
|
|
297
|
-
});
|
|
275
|
+
case 'review': {
|
|
276
|
+
if (!args.proposalId) throw new Error('proposalId is required to answer a proposal');
|
|
277
|
+
if (!args.accept?.length && !args.reject?.length) {
|
|
278
|
+
throw new Error('Say which changes you are taking (accept) and which you are not (reject)');
|
|
298
279
|
}
|
|
280
|
+
return callZephlyAPI('mcpReviewPlanProposal', {
|
|
281
|
+
proposalId: args.proposalId,
|
|
282
|
+
accept: args.accept || [],
|
|
283
|
+
reject: args.reject || [],
|
|
284
|
+
note: args.note,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
299
287
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
288
|
+
case 'withdraw': {
|
|
289
|
+
if (!args.proposalId) throw new Error('proposalId is required to take back a proposal');
|
|
290
|
+
return callZephlyAPI('mcpReviewPlanProposal', { proposalId: args.proposalId, withdraw: true });
|
|
291
|
+
}
|
|
304
292
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
default:
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Unknown action "${action}". Use propose, list, get, review or withdraw.`
|
|
296
|
+
);
|
|
309
297
|
}
|
|
310
298
|
}
|
package/handlers/tasks.js
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
* Code links are derived: the files a task touches (changedFiles / linkedFiles)
|
|
8
8
|
* resolve to the features that own those paths (E-258). Agents name the feature
|
|
9
9
|
* the work advances through `links`.
|
|
10
|
-
* Tags
|
|
10
|
+
* Tags: only the tagIds the caller passes are applied; keyword matches against
|
|
11
|
+
* the local project cache come back as `suggestedTags` (see lib/auto-assign.js).
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { previewEntityLinks, partitionProposals, attachSuggestionIds } from '../lib/autolink.js';
|
|
14
15
|
import { callZephlyAPI } from '../lib/http-client.js';
|
|
15
16
|
import { resolveTaskAutoAssign } from '../lib/auto-assign.js';
|
|
17
|
+
import { suggestTags } from '../lib/suggested-tags.js';
|
|
16
18
|
import { buildTaskUrl } from '../lib/web-url.js';
|
|
17
19
|
import { getLogger } from '../lib/logger.js';
|
|
18
20
|
import { writeActiveSession, clearActiveSession } from '../lib/active-session.js';
|
|
@@ -21,24 +23,6 @@ import { getContext } from './context-manifest.js';
|
|
|
21
23
|
import { getCommitFiles, getRepositoryRoot } from '../lib/git-helpers.js';
|
|
22
24
|
import { normalizeChangedFiles } from '../lib/changed-files.js';
|
|
23
25
|
|
|
24
|
-
/**
|
|
25
|
-
* Apply tags to a newly created entity via bulkTagEntities.
|
|
26
|
-
* Non-fatal — logs errors but doesn't throw.
|
|
27
|
-
*/
|
|
28
|
-
async function applyAutoTags(organizationId, entityType, entityId, tagIds) {
|
|
29
|
-
if (!organizationId || !tagIds?.length || !entityId) return;
|
|
30
|
-
try {
|
|
31
|
-
await callZephlyAPI('mcpBulkTagEntities', {
|
|
32
|
-
organizationId,
|
|
33
|
-
tagIds,
|
|
34
|
-
entities: [{ entityType, entityId }],
|
|
35
|
-
operation: 'add',
|
|
36
|
-
});
|
|
37
|
-
} catch (err) {
|
|
38
|
-
getLogger().warn('Auto-tag failed', { entityType, entityId, error: err.message });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
26
|
/**
|
|
43
27
|
* Dispatch manage_task actions to the appropriate handler
|
|
44
28
|
*/
|
|
@@ -54,10 +38,35 @@ export async function manageTask(args) {
|
|
|
54
38
|
case 'get_commits': return getTaskCommits(params);
|
|
55
39
|
case 'generate_how_it_works': return generateTaskHowItWorks(params);
|
|
56
40
|
case 'apply_how_it_works': return applyTaskHowItWorks(params);
|
|
41
|
+
case 'claim': return claimTask(params, false);
|
|
42
|
+
case 'release': return claimTask(params, true);
|
|
43
|
+
case 'list_suggested_edits': return suggestedEdits(params, false);
|
|
44
|
+
case 'answer_suggested_edit': return suggestedEdits(params, true);
|
|
57
45
|
default: throw new Error(`Unknown action: ${action}`);
|
|
58
46
|
}
|
|
59
47
|
}
|
|
60
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Claim a task, or give it back (E-259 #2747). The server decides who may and
|
|
51
|
+
* words the answer; a task someone else holds comes back as an error naming
|
|
52
|
+
* them, which is the agent's cue to pick other work rather than retry.
|
|
53
|
+
*/
|
|
54
|
+
async function claimTask({ taskId, claimNote }, release) {
|
|
55
|
+
if (!taskId) throw new Error(`taskId is required to ${release ? 'release' : 'claim'} a task`);
|
|
56
|
+
return callZephlyAPI('mcpClaimTask', release ? { taskId, release: true } : { taskId, note: claimNote });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Suggested edits on a claimed task (E-259 #2809): list what is waiting, or
|
|
61
|
+
* answer one. The server decides who may accept, reject or withdraw.
|
|
62
|
+
*/
|
|
63
|
+
async function suggestedEdits({ taskId, editId, answer, note }, answering) {
|
|
64
|
+
if (!taskId) throw new Error('taskId is required');
|
|
65
|
+
if (!answering) return callZephlyAPI('mcpTaskSuggestedEdits', { taskId });
|
|
66
|
+
if (!editId || !answer) throw new Error('editId and answer (accept, reject or withdraw) are required');
|
|
67
|
+
return callZephlyAPI('mcpTaskSuggestedEdits', { taskId, editId, answer, note });
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Bridge user terminology ("dashboard") to code locations by searching the
|
|
63
72
|
* manifest for the task's own words. Best-effort: returns null when there is
|
|
@@ -129,18 +138,13 @@ async function createTask(args) {
|
|
|
129
138
|
};
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
//
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
autoAssign.matchedTags.map((t) => t.id),
|
|
140
|
-
);
|
|
141
|
-
result.autoAssigned = {
|
|
142
|
-
tags: autoAssign.matchedTags.map((t) => t.name),
|
|
143
|
-
};
|
|
141
|
+
// Suggest, never apply, keyword-matched tags (#2830).
|
|
142
|
+
const suggestedTags = suggestTags(
|
|
143
|
+
await resolveTaskAutoAssign(projectId, title, description),
|
|
144
|
+
createArgs.tagIds,
|
|
145
|
+
);
|
|
146
|
+
if (suggestedTags.length && result?.taskId) {
|
|
147
|
+
result.suggestedTags = suggestedTags;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
if (autoContext) {
|
package/lib/auto-assign.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-assign Utility
|
|
3
|
-
* Matches task/epic content against cached tags
|
|
4
|
-
*
|
|
3
|
+
* Matches task/epic content against cached tags to SUGGEST tags at creation.
|
|
4
|
+
*
|
|
5
|
+
* Suggest-only, deliberately (#2830). The matcher fires on any word in the
|
|
6
|
+
* title or description, so it proposes tags like "notes" or "docs" on work
|
|
7
|
+
* that merely mentions them. It never actually applied anything before #2830
|
|
8
|
+
* (the API side was a stub), so applying its output would have switched on a
|
|
9
|
+
* noisy behaviour nobody had seen. The agent passes the tags it wants as
|
|
10
|
+
* `tagIds`; matches come back as `suggestedTags` for it to accept.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import { readConfig } from './local-cache.js';
|
package/lib/create-server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { listPrompts, getPromptContent } from '../prompts/index.js';
|
|
|
20
20
|
import { MCP_VERSION } from './version.js';
|
|
21
21
|
import { getLogger } from './logger.js';
|
|
22
22
|
import { isRemoteSafe } from './remote-tools.js';
|
|
23
|
+
import { annotate } from './tool-annotations.js';
|
|
23
24
|
import {
|
|
24
25
|
EMAIL_ALREADY_REGISTERED,
|
|
25
26
|
NOT_AUTHENTICATED,
|
|
@@ -84,7 +85,7 @@ export function createServer({ surface = 'local', startSignIn = defaultStartSign
|
|
|
84
85
|
// cannot disagree. They must agree: filtering only tools/list would leave
|
|
85
86
|
// every excluded handler dispatchable by a client that guesses the name,
|
|
86
87
|
// which is the failure this whole module exists to prevent.
|
|
87
|
-
const tools = surface === 'remote' ? TOOLS.filter((tool) => isRemoteSafe(tool.name)) : TOOLS;
|
|
88
|
+
const tools = (surface === 'remote' ? TOOLS.filter((tool) => isRemoteSafe(tool.name)) : TOOLS).map(annotate);
|
|
88
89
|
const available = new Set(tools.map((tool) => tool.name));
|
|
89
90
|
|
|
90
91
|
const server = new Server(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggested tags (#2830).
|
|
3
|
+
*
|
|
4
|
+
* Keyword matches from lib/auto-assign.js are returned to the agent as
|
|
5
|
+
* `suggestedTags` and never applied; see that file for why.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Turn keyword matches into the `suggestedTags` a create response carries,
|
|
10
|
+
* leaving out tags the caller already applied explicitly.
|
|
11
|
+
*
|
|
12
|
+
* @param {object|null} autoAssign - Result of resolveTaskAutoAssign / resolveEpicAutoAssign
|
|
13
|
+
* @param {string[]} [appliedTagIds] - Tag IDs sent with the create
|
|
14
|
+
* @returns {Array<{id: string, name: string}>}
|
|
15
|
+
*/
|
|
16
|
+
export function suggestTags(autoAssign, appliedTagIds = []) {
|
|
17
|
+
const applied = new Set(appliedTagIds || []);
|
|
18
|
+
return (autoAssign?.matchedTags || [])
|
|
19
|
+
.filter((t) => !applied.has(t.id))
|
|
20
|
+
.map((t) => ({ id: t.id, name: t.name }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool annotations: which tools only read (#2808).
|
|
3
|
+
*
|
|
4
|
+
* Without annotations a client must assume any tool may write or destroy data,
|
|
5
|
+
* which is what the MCP spec says an unannotated tool means. Claude uses the
|
|
6
|
+
* hint to sort a connector's tools into read-only and write/delete categories,
|
|
7
|
+
* so a policy that allows only read-only tools (Claude Team/Enterprise tool
|
|
8
|
+
* permissions) blocked EVERY tool here, reads included, while
|
|
9
|
+
* every tool was unannotated.
|
|
10
|
+
*
|
|
11
|
+
* A hint, not a control: the API enforces scopes and permissions whatever a
|
|
12
|
+
* client believes. So the list errs toward leaving a tool OUT. A write wrongly
|
|
13
|
+
* marked read-only would let it through a read-only policy, while a read left
|
|
14
|
+
* unmarked only costs the user an approval prompt.
|
|
15
|
+
*
|
|
16
|
+
* Membership was verified by tracing each handler to the endpoints it calls
|
|
17
|
+
* (config/endpoint-map.js): all GETs, or POSTs to query routes the API mounts
|
|
18
|
+
* behind OptionalReadScopeMiddleware (projects/story, organization/analyze,
|
|
19
|
+
* graph/*, tags/suggest, links/preview, attachments/download-url). Some have
|
|
20
|
+
* local side effects that change no EzModo data, and those count as reads:
|
|
21
|
+
* get_document caches the content into .ezmodo/docs, get_current_project_context
|
|
22
|
+
* and list_tags cache tags, and detect_git_repository and list_project_worktrees
|
|
23
|
+
* run read-only git commands.
|
|
24
|
+
*
|
|
25
|
+
* Adding a tool? If it only reads, add it here.
|
|
26
|
+
* __tests__/tool-annotations.test.js fails when a get_/list_/search_ tool is
|
|
27
|
+
* left unclassified, so a new read cannot silently default to "may write".
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
31
|
+
'detect_git_repository',
|
|
32
|
+
'estimate_task',
|
|
33
|
+
'evaluate_feature_flag',
|
|
34
|
+
'get_access',
|
|
35
|
+
'get_ai_insights',
|
|
36
|
+
'get_attachment_url',
|
|
37
|
+
'get_catalog',
|
|
38
|
+
'get_catalog_diff',
|
|
39
|
+
'get_context',
|
|
40
|
+
'get_current_project_context',
|
|
41
|
+
'get_decision',
|
|
42
|
+
'get_design',
|
|
43
|
+
'get_design_system',
|
|
44
|
+
'get_document',
|
|
45
|
+
'get_document_template',
|
|
46
|
+
'get_epic',
|
|
47
|
+
'get_epic_activity',
|
|
48
|
+
'get_epic_plan',
|
|
49
|
+
'get_feature',
|
|
50
|
+
'get_feature_flag',
|
|
51
|
+
'get_goal',
|
|
52
|
+
'get_graph',
|
|
53
|
+
'get_manifest_schema',
|
|
54
|
+
'get_milestone',
|
|
55
|
+
'get_org_areas',
|
|
56
|
+
'get_organization',
|
|
57
|
+
'get_project',
|
|
58
|
+
'get_project_changes',
|
|
59
|
+
'get_project_story',
|
|
60
|
+
'get_task',
|
|
61
|
+
'get_testing_summary',
|
|
62
|
+
'infer_dependencies',
|
|
63
|
+
'list_agent_suggestions',
|
|
64
|
+
'list_attachments',
|
|
65
|
+
'list_catalog_items',
|
|
66
|
+
'list_catalogs',
|
|
67
|
+
'list_designs',
|
|
68
|
+
'list_epic_comments',
|
|
69
|
+
'list_epics',
|
|
70
|
+
'list_facts',
|
|
71
|
+
'list_feature_flags',
|
|
72
|
+
'list_folders',
|
|
73
|
+
'list_links',
|
|
74
|
+
'list_notifications',
|
|
75
|
+
'list_org_documents',
|
|
76
|
+
'list_project_worktrees',
|
|
77
|
+
'list_repositories',
|
|
78
|
+
'list_tags',
|
|
79
|
+
'list_test_cases',
|
|
80
|
+
'list_test_suites',
|
|
81
|
+
'list_todos',
|
|
82
|
+
'list_unmapped_paths',
|
|
83
|
+
'list_watched',
|
|
84
|
+
'preview_links',
|
|
85
|
+
'resolve_concepts',
|
|
86
|
+
'search_epics',
|
|
87
|
+
'search_features',
|
|
88
|
+
'search_tasks',
|
|
89
|
+
'validate_manifest',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The tool definition as a client should see it: read-only tools carry
|
|
94
|
+
* `annotations.readOnlyHint: true`; everything else is returned unchanged.
|
|
95
|
+
* Never mutates the shared definition in tools/.
|
|
96
|
+
*
|
|
97
|
+
* @template {{ name: string, annotations?: object }} T
|
|
98
|
+
* @param {T} tool
|
|
99
|
+
* @returns {T}
|
|
100
|
+
*/
|
|
101
|
+
export function annotate(tool) {
|
|
102
|
+
if (!READ_ONLY_TOOLS.has(tool.name)) return tool;
|
|
103
|
+
return { ...tool, annotations: { ...tool.annotations, readOnlyHint: true } };
|
|
104
|
+
}
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
package/tools/epics.js
CHANGED
|
@@ -44,7 +44,8 @@ export const EPIC_TOOLS = [
|
|
|
44
44
|
'IMPORTANT when creating: Call get_context first for each area the epic covers to discover relevant ' +
|
|
45
45
|
'files and integration points. Include in the description which layers/services are affected and ' +
|
|
46
46
|
'reference specific file paths. Create child tasks that each reference specific files from context queries. ' +
|
|
47
|
-
'
|
|
47
|
+
'Tags: pass the tag IDs you want as tagIds; the response lists keyword-matched tags you did not pass as ' +
|
|
48
|
+
'suggestedTags, which are NOT applied — add any that fit with an update. ' +
|
|
48
49
|
'To align an epic to an organization goal, link it to a milestone that is linked to that goal ' +
|
|
49
50
|
'(set milestoneId) — epics have no direct goal field. ' +
|
|
50
51
|
'BREAKING DOWN A FEATURE: pass the child tasks in the `tasks` array on create and the epic and its ' +
|
|
@@ -56,8 +57,12 @@ export const EPIC_TOOLS = [
|
|
|
56
57
|
properties: {
|
|
57
58
|
action: {
|
|
58
59
|
type: 'string',
|
|
59
|
-
enum: ['create', 'update', 'generate_how_it_works', 'apply_how_it_works'],
|
|
60
|
-
description: 'Action to perform. "
|
|
60
|
+
enum: ['create', 'update', 'generate_how_it_works', 'apply_how_it_works', 'add_editor', 'remove_editor'],
|
|
61
|
+
description: 'Action to perform. "add_editor" / "remove_editor" (epicId + editorUserId) choose who ' +
|
|
62
|
+
'else may change the epic\'s plan, decide its questions and answer suggested changes (E-259). ' +
|
|
63
|
+
'Only the owner, the creator or an org admin may choose; an editor may remove themselves. ' +
|
|
64
|
+
'get_epic lists the current editors. ' +
|
|
65
|
+
'"generate_how_it_works" (re)generates the epic\'s grounded, ' +
|
|
61
66
|
'source-attributed "how it works" living description from its reality — its tasks\' status ' +
|
|
62
67
|
'rollup, their captured decisions and their linked commits (requires epicId; AI-quota gated). ' +
|
|
63
68
|
'An epic is where intent and reality drift furthest apart: intent is a charter written once, ' +
|
|
@@ -68,7 +73,12 @@ export const EPIC_TOOLS = [
|
|
|
68
73
|
// --- Identifiers ---
|
|
69
74
|
epicId: {
|
|
70
75
|
type: 'string',
|
|
71
|
-
description: 'Epic ID (required for update, generate_how_it_works, apply_how_it_works
|
|
76
|
+
description: 'Epic ID (required for update, generate_how_it_works, apply_how_it_works, ' +
|
|
77
|
+
'add_editor, remove_editor)',
|
|
78
|
+
},
|
|
79
|
+
editorUserId: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'add_editor / remove_editor: the user to add or remove (must be in the organization)',
|
|
72
82
|
},
|
|
73
83
|
markdown: {
|
|
74
84
|
type: 'string',
|
|
@@ -137,10 +147,11 @@ export const EPIC_TOOLS = [
|
|
|
137
147
|
name: { type: 'string' },
|
|
138
148
|
},
|
|
139
149
|
},
|
|
140
|
-
|
|
150
|
+
tagIds: {
|
|
141
151
|
type: 'array',
|
|
142
152
|
items: { type: 'string' },
|
|
143
|
-
description: '
|
|
153
|
+
description: 'Tag IDs for categorization (get them from get_current_project_context). ' +
|
|
154
|
+
'Used by create and update; on update this REPLACES the set.',
|
|
144
155
|
},
|
|
145
156
|
color: {
|
|
146
157
|
type: 'string',
|
|
@@ -255,7 +266,8 @@ export const EPIC_TOOLS = [
|
|
|
255
266
|
'Responses include `descriptionDocumentId` — the id of the backing rich-description Document ' +
|
|
256
267
|
'when the description has been promoted to one (E-189), otherwise omitted. ' +
|
|
257
268
|
'Responses also include `derivedGoalIds` — the organization goal(s) this epic aligns to, ' +
|
|
258
|
-
'derived via its milestone (epics have no direct goal field).'
|
|
269
|
+
'derived via its milestone (epics have no direct goal field). ' +
|
|
270
|
+
'`editors` lists the people besides the owner who may change its plan (E-259).',
|
|
259
271
|
inputSchema: {
|
|
260
272
|
type: 'object',
|
|
261
273
|
properties: {
|
|
@@ -390,12 +402,18 @@ export const EPIC_TOOLS = [
|
|
|
390
402
|
name: 'get_epic_activity',
|
|
391
403
|
description: 'Catch me up on an epic (E-259): what changed since YOU last looked. Returns `summary`, ' +
|
|
392
404
|
'plain sentences you can relay to your person as-is, most important first: decisions waiting on ' +
|
|
393
|
-
'their view,
|
|
405
|
+
'their view, open questions put to them and open objections, comments that mention or reply to ' +
|
|
406
|
+
'them, decisions made, new plan versions (who ' +
|
|
394
407
|
'changed what, and which AI did it for them), and tasks added, started, finished or blocked. ' +
|
|
395
408
|
'The details are alongside. Their own changes are left out. Call it when you start or resume ' +
|
|
396
409
|
'work on an epic other people also work on, and before changing its plan. By default this also ' +
|
|
397
410
|
'marks the epic as caught up, so the next call shows only newer changes; pass markSeen:false to ' +
|
|
398
|
-
'look without that
|
|
411
|
+
'look without that.\n\n' +
|
|
412
|
+
'LIVE: pass waitSeconds (up to 25) to wait for something to happen instead of hearing ' +
|
|
413
|
+
'"nothing has changed" — the call returns as soon as someone changes the plan, comments, ' +
|
|
414
|
+
'decides, or picks up or moves a task. Call it again to keep following a shared session. ' +
|
|
415
|
+
'`hereNow` says who else is on the epic right now, people and their AIs; calling any epic tool ' +
|
|
416
|
+
'shows you there too.',
|
|
399
417
|
inputSchema: {
|
|
400
418
|
type: 'object',
|
|
401
419
|
properties: {
|
|
@@ -409,6 +427,10 @@ export const EPIC_TOOLS = [
|
|
|
409
427
|
type: 'boolean',
|
|
410
428
|
description: 'Mark the epic as caught up after answering (default true)',
|
|
411
429
|
},
|
|
430
|
+
waitSeconds: {
|
|
431
|
+
type: 'number',
|
|
432
|
+
description: 'Wait up to this many seconds (max 25) for something new before answering',
|
|
433
|
+
},
|
|
412
434
|
},
|
|
413
435
|
required: ['epicId'],
|
|
414
436
|
},
|
|
@@ -417,12 +439,15 @@ export const EPIC_TOOLS = [
|
|
|
417
439
|
name: 'list_epic_comments',
|
|
418
440
|
description: 'Read an epic\'s discussion (E-259), oldest first. Each comment says who wrote it and, ' +
|
|
419
441
|
'when an AI wrote it for them, which AI (`agentName`). Replies carry `parentId`. ' +
|
|
420
|
-
'Read this before planning or changing a shared epic: other people\'s questions and objections live here.'
|
|
442
|
+
'Read this before planning or changing a shared epic: other people\'s questions and objections live here. ' +
|
|
443
|
+
'Each comment has a `kind` (comment, question, objection, alternative); the last three stay open until ' +
|
|
444
|
+
'`resolvedAt` is set. Pass open:true for only the ones still waiting — check it before approving a plan.',
|
|
421
445
|
inputSchema: {
|
|
422
446
|
type: 'object',
|
|
423
447
|
properties: {
|
|
424
448
|
epicId: { type: 'string', description: 'The epic ID (required)' },
|
|
425
449
|
limit: { type: 'number', description: 'Maximum comments to return (default 100, max 500)' },
|
|
450
|
+
open: { type: 'boolean', description: 'Only the questions, objections and alternatives not yet settled' },
|
|
426
451
|
},
|
|
427
452
|
required: ['epicId'],
|
|
428
453
|
},
|
|
@@ -432,7 +457,11 @@ export const EPIC_TOOLS = [
|
|
|
432
457
|
description: 'Post to an epic\'s discussion (E-259), or reply to a comment with `parentId`. ' +
|
|
433
458
|
'Posted as the person whose key you use, marked as written by you. ' +
|
|
434
459
|
'Mentioned people, the author you reply to and everyone following the epic are notified, ' +
|
|
435
|
-
'and posting makes that person follow it. Write plainly: one point per comment, readable by anyone
|
|
460
|
+
'and posting makes that person follow it. Write plainly: one point per comment, readable by anyone.\n\n' +
|
|
461
|
+
'Say what the comment is with `kind`: a `question` you need answered, an `objection` to the plan, or ' +
|
|
462
|
+
'an `alternative` approach. Those stay open until settled, show up in catch me up, and an open objection ' +
|
|
463
|
+
'warns whoever approves the plan. To answer one, reply with parentId and resolvesParent:true ' +
|
|
464
|
+
'(the person who raised it, or the epic\'s owner, may settle it; anyone may reply).',
|
|
436
465
|
inputSchema: {
|
|
437
466
|
type: 'object',
|
|
438
467
|
properties: {
|
|
@@ -440,6 +469,15 @@ export const EPIC_TOOLS = [
|
|
|
440
469
|
content: { type: 'string', description: 'The comment (markdown)' },
|
|
441
470
|
parentId: { type: 'string', description: 'Reply to this comment' },
|
|
442
471
|
mentions: { type: 'array', items: { type: 'string' }, description: 'User IDs to notify' },
|
|
472
|
+
kind: {
|
|
473
|
+
type: 'string',
|
|
474
|
+
enum: ['comment', 'question', 'objection', 'alternative'],
|
|
475
|
+
description: 'What this is (default comment). Replies are always comments.',
|
|
476
|
+
},
|
|
477
|
+
resolvesParent: {
|
|
478
|
+
type: 'boolean',
|
|
479
|
+
description: 'With parentId: this reply settles the question, objection or alternative it answers',
|
|
480
|
+
},
|
|
443
481
|
},
|
|
444
482
|
required: ['epicId', 'content'],
|
|
445
483
|
},
|
|
@@ -453,7 +491,7 @@ export const EPIC_TOOLS = [
|
|
|
453
491
|
'take some and leave others. Nothing changes until they do.\n' +
|
|
454
492
|
'list: what is waiting on an epic. Open ones come first, and each change that no longer fits the ' +
|
|
455
493
|
'current plan is flagged with the reason.\n' +
|
|
456
|
-
'review (owner, creator or org admin only): `accept` and `reject` name changes by their op id. ' +
|
|
494
|
+
'review (owner, editors, creator or org admin only): `accept` and `reject` name changes by their op id. ' +
|
|
457
495
|
'A change you name in neither is left for later and the proposal stays open. A change whose task ' +
|
|
458
496
|
'someone has since removed is reported back as stale rather than quietly reapplied.\n' +
|
|
459
497
|
'withdraw: take back a proposal you made.',
|
package/tools/tasks.js
CHANGED
|
@@ -32,14 +32,51 @@ export const TASK_TOOLS = [
|
|
|
32
32
|
properties: {
|
|
33
33
|
action: {
|
|
34
34
|
type: 'string',
|
|
35
|
-
enum: [
|
|
36
|
-
|
|
35
|
+
enum: [
|
|
36
|
+
'create', 'update', 'complete', 'defer', 'link_commit', 'unlink_commit', 'get_commits',
|
|
37
|
+
'generate_how_it_works', 'apply_how_it_works', 'claim', 'release',
|
|
38
|
+
'list_suggested_edits', 'answer_suggested_edit',
|
|
39
|
+
],
|
|
40
|
+
description: 'Action to perform. "claim" (taskId, optional claimNote) says you are working on ' +
|
|
41
|
+
'this task, so other people\'s AIs on the same epic pick different work (E-259). Claim ' +
|
|
42
|
+
'BEFORE starting a task on a shared epic. It lasts 2 hours and renews while you update ' +
|
|
43
|
+
'the task or link commits; claiming again renews it. If someone else holds it you are ' +
|
|
44
|
+
'told who and which branch their work is on — pick another task. The answer warns about ' +
|
|
45
|
+
'other in-progress tasks on the epic that touch the same files, with the branch each is on, ' +
|
|
46
|
+
'so you can build on that work or keep clear of it. "release" gives it back when you stop. ' +
|
|
47
|
+
'TASK RULES: on a task someone ELSE has claimed, changing its title, description, steps or ' +
|
|
48
|
+
'epic, or deleting it, is sent to them as a suggested edit (the response says ' +
|
|
49
|
+
'`suggested: true`; anything else in the same update still applies), and changing its ' +
|
|
50
|
+
'status, assignee or step progress is refused — leave a comment instead. The claimer and ' +
|
|
51
|
+
'whoever runs the epic are not limited. "list_suggested_edits" (taskId) shows what is ' +
|
|
52
|
+
'waiting on a task; "answer_suggested_edit" (taskId, editId, answer: accept|reject|withdraw, ' +
|
|
53
|
+
'optional note) answers one — the claimer or whoever runs the epic accepts or rejects, the ' +
|
|
54
|
+
'author withdraws. ' +
|
|
55
|
+
'"generate_how_it_works" (re)generates the task\'s ' +
|
|
37
56
|
'grounded, source-attributed "how it works" living description from its reality — ' +
|
|
38
57
|
'subtasks, comments, status history, commits plus a manifest pass over linked files ' +
|
|
39
58
|
'(requires taskId; AI-quota gated). "apply_how_it_works" (BYO-AI) persists a summary ' +
|
|
40
59
|
'YOU authored: pass markdown + sources; the server validates your cited sources against ' +
|
|
41
60
|
'the real grounded context (dropping fabricated ones) before saving — no server model call.',
|
|
42
61
|
},
|
|
62
|
+
editId: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'answer_suggested_edit: the suggested edit to answer',
|
|
65
|
+
},
|
|
66
|
+
answer: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: ['accept', 'reject', 'withdraw'],
|
|
69
|
+
description: 'answer_suggested_edit: accept or reject it (the claimer or whoever runs the epic), ' +
|
|
70
|
+
'or withdraw your own',
|
|
71
|
+
},
|
|
72
|
+
note: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'answer_suggested_edit: what you want to say back, optional',
|
|
75
|
+
},
|
|
76
|
+
claimNote: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'claim: what you are about to do, in a few words (shown to the others)',
|
|
79
|
+
},
|
|
43
80
|
// --- Identifiers (used by most actions) ---
|
|
44
81
|
taskId: {
|
|
45
82
|
type: 'string',
|