@ferrtrack/mcp 10.13.2 → 10.13.3
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/dist/tools/__tests__/annotations.test.d.ts +2 -0
- package/dist/tools/__tests__/annotations.test.d.ts.map +1 -0
- package/dist/tools/__tests__/annotations.test.js +56 -0
- package/dist/tools/__tests__/writes.test.js +45 -2
- package/dist/tools/comments.d.ts.map +1 -1
- package/dist/tools/comments.js +2 -2
- package/dist/tools/cycles.d.ts.map +1 -1
- package/dist/tools/cycles.js +9 -19
- package/dist/tools/issue-details.d.ts.map +1 -1
- package/dist/tools/issue-details.js +1 -1
- package/dist/tools/issues.d.ts.map +1 -1
- package/dist/tools/issues.js +2 -2
- package/dist/tools/milestones.d.ts.map +1 -1
- package/dist/tools/milestones.js +12 -21
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +2 -2
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotations.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/annotations.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
process.env.FERRLABS_API_TOKEN = 'test-token';
|
|
3
|
+
const annotations = new Map();
|
|
4
|
+
const mockServer = {
|
|
5
|
+
tool: vi.fn((name, _desc, _schema, ...rest) => {
|
|
6
|
+
annotations.set(name, rest.length > 1 ? rest[0] : undefined);
|
|
7
|
+
}),
|
|
8
|
+
};
|
|
9
|
+
const DESTRUCTIVE = ['delete_issue_comment', 'delete_cycle', 'delete_milestone'];
|
|
10
|
+
const READ_ONLY = [
|
|
11
|
+
'list_track_projects',
|
|
12
|
+
'get_project',
|
|
13
|
+
'list_track_issues',
|
|
14
|
+
'get_issue',
|
|
15
|
+
'list_issue_links',
|
|
16
|
+
'list_issue_comments',
|
|
17
|
+
'list_cycles',
|
|
18
|
+
'list_cycle_issues',
|
|
19
|
+
'list_milestones',
|
|
20
|
+
'list_track_users',
|
|
21
|
+
'search_track',
|
|
22
|
+
];
|
|
23
|
+
describe('tool annotations', async () => {
|
|
24
|
+
const { registerProjectTools } = await import('../projects.js');
|
|
25
|
+
const { registerIssueDetailsTool } = await import('../issue-details.js');
|
|
26
|
+
const { registerIssueTools } = await import('../issues.js');
|
|
27
|
+
const { registerCommentTools } = await import('../comments.js');
|
|
28
|
+
const { registerCycleTools } = await import('../cycles.js');
|
|
29
|
+
const { registerMilestoneTools } = await import('../milestones.js');
|
|
30
|
+
const { registerSearchTools } = await import('../search.js');
|
|
31
|
+
registerProjectTools(mockServer);
|
|
32
|
+
registerIssueDetailsTool(mockServer);
|
|
33
|
+
registerIssueTools(mockServer);
|
|
34
|
+
registerCommentTools(mockServer);
|
|
35
|
+
registerCycleTools(mockServer);
|
|
36
|
+
registerMilestoneTools(mockServer);
|
|
37
|
+
registerSearchTools(mockServer);
|
|
38
|
+
it.each(DESTRUCTIVE)('%s is marked destructive', (name) => {
|
|
39
|
+
expect(annotations.get(name)?.destructiveHint).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it.each(READ_ONLY)('%s is marked read-only', (name) => {
|
|
42
|
+
expect(annotations.get(name)?.readOnlyHint).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('marks no other tool destructive or read-only', () => {
|
|
45
|
+
const flagged = new Set([...DESTRUCTIVE, ...READ_ONLY]);
|
|
46
|
+
for (const [name, a] of annotations) {
|
|
47
|
+
if (!flagged.has(name)) {
|
|
48
|
+
expect(a?.destructiveHint, name).toBeUndefined();
|
|
49
|
+
expect(a?.readOnlyHint, name).toBeUndefined();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('covers every registered tool', () => {
|
|
54
|
+
expect(annotations.size).toBe(DESTRUCTIVE.length + READ_ONLY.length + 11);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -3,8 +3,8 @@ process.env.FERRLABS_API_TOKEN = 'test-token';
|
|
|
3
3
|
const handlers = new Map();
|
|
4
4
|
const schemas = new Map();
|
|
5
5
|
const mockServer = {
|
|
6
|
-
tool: vi.fn((name, _desc, schema,
|
|
7
|
-
handlers.set(name,
|
|
6
|
+
tool: vi.fn((name, _desc, schema, ...rest) => {
|
|
7
|
+
handlers.set(name, rest[rest.length - 1]);
|
|
8
8
|
schemas.set(name, schema);
|
|
9
9
|
}),
|
|
10
10
|
};
|
|
@@ -36,9 +36,11 @@ describe('ferrtrack write tools', () => {
|
|
|
36
36
|
mockFetch.mockResolvedValue(ok());
|
|
37
37
|
const { registerIssueTools } = await import('../issues.js');
|
|
38
38
|
const { registerCycleTools } = await import('../cycles.js');
|
|
39
|
+
const { registerMilestoneTools } = await import('../milestones.js');
|
|
39
40
|
const { registerCommentTools } = await import('../comments.js');
|
|
40
41
|
registerIssueTools(mockServer);
|
|
41
42
|
registerCycleTools(mockServer);
|
|
43
|
+
registerMilestoneTools(mockServer);
|
|
42
44
|
registerCommentTools(mockServer);
|
|
43
45
|
});
|
|
44
46
|
it('negotiates the contract by date header, with no version left in the path', async () => {
|
|
@@ -98,4 +100,45 @@ describe('ferrtrack write tools', () => {
|
|
|
98
100
|
expect(status.safeParse('closed').success).toBe(true);
|
|
99
101
|
expect(status.safeParse('done').success).toBe(false);
|
|
100
102
|
});
|
|
103
|
+
it('does not expose get tools for routes the API does not serve', () => {
|
|
104
|
+
expect(handlers.has('get_cycle')).toBe(false);
|
|
105
|
+
expect(handlers.has('get_milestone')).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
it('create_cycle requires ISO datetimes, since the API rejects plain dates', () => {
|
|
108
|
+
const { starts_at, ends_at } = schemas.get('create_cycle');
|
|
109
|
+
expect(starts_at.safeParse('2026-01-05T00:00:00Z').success).toBe(true);
|
|
110
|
+
expect(starts_at.safeParse('2026-01-05T00:00:00+02:00').success).toBe(true);
|
|
111
|
+
expect(starts_at.safeParse('2026-01-05').success).toBe(false);
|
|
112
|
+
expect(ends_at.safeParse('2026-01-19').success).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
it('update_cycle has no status field, since status is derived from dates', () => {
|
|
115
|
+
expect(schemas.get('update_cycle')).not.toHaveProperty('status');
|
|
116
|
+
});
|
|
117
|
+
it('create_milestone sends summary and target_date', async () => {
|
|
118
|
+
await handlers.get('create_milestone')({
|
|
119
|
+
project_slug: 'web',
|
|
120
|
+
name: 'v1',
|
|
121
|
+
summary: 'first release',
|
|
122
|
+
target_date: '2026-03-01',
|
|
123
|
+
});
|
|
124
|
+
const call = lastCall();
|
|
125
|
+
expect(call.url).toBe(`${TRACK}/projects/web/milestones`);
|
|
126
|
+
expect(call.body).toEqual({ name: 'v1', summary: 'first release', target_date: '2026-03-01' });
|
|
127
|
+
});
|
|
128
|
+
it('update_milestone patches summary and target_date and nothing else', async () => {
|
|
129
|
+
await handlers.get('update_milestone')({ milestone_id: 'm1', target_date: null });
|
|
130
|
+
const call = lastCall();
|
|
131
|
+
expect(call.url).toBe(`${TRACK}/milestones/m1`);
|
|
132
|
+
expect(call.method).toBe('PATCH');
|
|
133
|
+
expect(call.body).toEqual({ target_date: null });
|
|
134
|
+
});
|
|
135
|
+
it('update_milestone accepts the API status values only', () => {
|
|
136
|
+
const { status } = schemas.get('update_milestone');
|
|
137
|
+
for (const ok of ['planned', 'in_progress', 'shipped', 'canceled']) {
|
|
138
|
+
expect(status.safeParse(ok).success).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
for (const bad of ['open', 'completed', 'cancelled']) {
|
|
141
|
+
expect(status.safeParse(bad).success).toBe(false);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
101
144
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/tools/comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAY1E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/tools/comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAY1E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,QA8ErD"}
|
package/dist/tools/comments.js
CHANGED
|
@@ -4,7 +4,7 @@ import { trackRequest } from '../api-base.js';
|
|
|
4
4
|
export function registerCommentTools(server) {
|
|
5
5
|
server.tool('list_issue_comments', 'List all comments on a FerrTrack issue by its ref (e.g. "FT-12").', {
|
|
6
6
|
issue_ref: z.string().min(1).describe('Issue ref, e.g. "FT-12"'),
|
|
7
|
-
}, async ({ issue_ref }) => {
|
|
7
|
+
}, { readOnlyHint: true }, async ({ issue_ref }) => {
|
|
8
8
|
const token = await getToken();
|
|
9
9
|
const comments = await trackRequest(`/issues/${encodeURIComponent(issue_ref)}/comments`, { token });
|
|
10
10
|
return {
|
|
@@ -35,7 +35,7 @@ export function registerCommentTools(server) {
|
|
|
35
35
|
server.tool('delete_issue_comment', 'Delete a comment you authored on a FerrTrack issue.', {
|
|
36
36
|
issue_ref: z.string().min(1).describe('Issue ref, e.g. "FT-12"'),
|
|
37
37
|
comment_id: z.string().min(1).describe('Comment id'),
|
|
38
|
-
}, async ({ issue_ref, comment_id }) => {
|
|
38
|
+
}, { destructiveHint: true }, async ({ issue_ref, comment_id }) => {
|
|
39
39
|
const token = await getToken();
|
|
40
40
|
await trackRequest(`/issues/${encodeURIComponent(issue_ref)}/comments/${encodeURIComponent(comment_id)}`, { token, method: 'DELETE' });
|
|
41
41
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cycles.d.ts","sourceRoot":"","sources":["../../src/tools/cycles.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"cycles.d.ts","sourceRoot":"","sources":["../../src/tools/cycles.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAwB1E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QA8HnD"}
|
package/dist/tools/cycles.js
CHANGED
|
@@ -2,18 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { trackRequest } from '../api-base.js';
|
|
4
4
|
export function registerCycleTools(server) {
|
|
5
|
-
server.tool('get_cycle', 'Get a single FerrTrack cycle by id.', {
|
|
6
|
-
cycle_id: z.string().min(1).describe('Cycle id'),
|
|
7
|
-
}, async ({ cycle_id }) => {
|
|
8
|
-
const token = await getToken();
|
|
9
|
-
const cycle = await trackRequest(`/cycles/${encodeURIComponent(cycle_id)}`, { token });
|
|
10
|
-
return {
|
|
11
|
-
content: [{ type: 'text', text: toToolText(cycle) }],
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
5
|
server.tool('list_cycles', 'List cycles (sprints) for a FerrTrack project.', {
|
|
15
6
|
project_slug: z.string().min(1).describe('Project slug'),
|
|
16
|
-
}, async ({ project_slug }) => {
|
|
7
|
+
}, { readOnlyHint: true }, async ({ project_slug }) => {
|
|
17
8
|
const token = await getToken();
|
|
18
9
|
const cycles = await trackRequest(`/projects/${encodeURIComponent(project_slug)}/cycles`, { token });
|
|
19
10
|
return {
|
|
@@ -22,18 +13,18 @@ export function registerCycleTools(server) {
|
|
|
22
13
|
});
|
|
23
14
|
server.tool('list_cycle_issues', 'List the issues currently assigned to a FerrTrack cycle.', {
|
|
24
15
|
cycle_id: z.string().min(1).describe('Cycle id'),
|
|
25
|
-
}, async ({ cycle_id }) => {
|
|
16
|
+
}, { readOnlyHint: true }, async ({ cycle_id }) => {
|
|
26
17
|
const token = await getToken();
|
|
27
18
|
const issues = await trackRequest(`/cycles/${encodeURIComponent(cycle_id)}/issues`, { token });
|
|
28
19
|
return {
|
|
29
20
|
content: [{ type: 'text', text: toToolText(issues) }],
|
|
30
21
|
};
|
|
31
22
|
});
|
|
32
|
-
server.tool('create_cycle', 'Create a new cycle in a project. Dates are ISO 8601 (
|
|
23
|
+
server.tool('create_cycle', 'Create a new cycle in a project. Dates are ISO 8601 datetimes (e.g. 2026-01-05T00:00:00Z).', {
|
|
33
24
|
project_slug: z.string().min(1).describe('Project slug'),
|
|
34
25
|
name: z.string().min(1).max(100),
|
|
35
|
-
starts_at: z.
|
|
36
|
-
ends_at: z.
|
|
26
|
+
starts_at: z.iso.datetime({ offset: true }).describe('ISO datetime — cycle start.'),
|
|
27
|
+
ends_at: z.iso.datetime({ offset: true }).describe('ISO datetime — cycle end.'),
|
|
37
28
|
}, async ({ project_slug, name, starts_at, ends_at }) => {
|
|
38
29
|
const token = await getToken();
|
|
39
30
|
const cycle = await trackRequest(`/projects/${encodeURIComponent(project_slug)}/cycles`, {
|
|
@@ -54,12 +45,11 @@ export function registerCycleTools(server) {
|
|
|
54
45
|
content: [{ type: 'text', text: toToolText(cycle) }],
|
|
55
46
|
};
|
|
56
47
|
});
|
|
57
|
-
server.tool('update_cycle', 'Patch a cycle — rename it, shift dates
|
|
48
|
+
server.tool('update_cycle', 'Patch a cycle — rename it, or shift dates. Status is derived from the dates. Only fields you pass are touched.', {
|
|
58
49
|
cycle_id: z.string().min(1).describe('Cycle id'),
|
|
59
50
|
name: z.string().min(1).max(100).optional(),
|
|
60
|
-
starts_at: z.
|
|
61
|
-
ends_at: z.
|
|
62
|
-
status: z.enum(['planned', 'active', 'completed']).optional(),
|
|
51
|
+
starts_at: z.iso.datetime({ offset: true }).optional(),
|
|
52
|
+
ends_at: z.iso.datetime({ offset: true }).optional(),
|
|
63
53
|
}, async ({ cycle_id, ...patch }) => {
|
|
64
54
|
const token = await getToken();
|
|
65
55
|
const body = {};
|
|
@@ -78,7 +68,7 @@ export function registerCycleTools(server) {
|
|
|
78
68
|
});
|
|
79
69
|
server.tool('delete_cycle', 'Delete a cycle. Issues that were on it become un-cycled.', {
|
|
80
70
|
cycle_id: z.string().min(1).describe('Cycle id'),
|
|
81
|
-
}, async ({ cycle_id }) => {
|
|
71
|
+
}, { destructiveHint: true }, async ({ cycle_id }) => {
|
|
82
72
|
const token = await getToken();
|
|
83
73
|
await trackRequest(`/cycles/${encodeURIComponent(cycle_id)}`, {
|
|
84
74
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue-details.d.ts","sourceRoot":"","sources":["../../src/tools/issue-details.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAoB1E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"issue-details.d.ts","sourceRoot":"","sources":["../../src/tools/issue-details.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAoB1E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,QAkBzD"}
|
|
@@ -4,7 +4,7 @@ import { trackRequest } from '../api-base.js';
|
|
|
4
4
|
export function registerIssueDetailsTool(server) {
|
|
5
5
|
server.tool('get_issue', 'Get the full content of a FerrTrack issue by its ref (e.g. "FT-12") — title, body, status, kind, labels, assignee.', {
|
|
6
6
|
issue_ref: z.string().min(1).describe('Issue ref, e.g. "FT-12"'),
|
|
7
|
-
}, async ({ issue_ref }) => {
|
|
7
|
+
}, { readOnlyHint: true }, async ({ issue_ref }) => {
|
|
8
8
|
const token = await getToken();
|
|
9
9
|
const issue = await trackRequest(`/issues/${encodeURIComponent(issue_ref)}`, {
|
|
10
10
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issues.d.ts","sourceRoot":"","sources":["../../src/tools/issues.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAG1E,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAWlB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"issues.d.ts","sourceRoot":"","sources":["../../src/tools/issues.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAG1E,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAWlB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAuHnD"}
|
package/dist/tools/issues.js
CHANGED
|
@@ -36,7 +36,7 @@ export function registerIssueTools(server) {
|
|
|
36
36
|
.max(200)
|
|
37
37
|
.optional()
|
|
38
38
|
.describe('Max issues to return (default 50).'),
|
|
39
|
-
}, async ({ project_slug, status, kind, assignee_id, limit }) => {
|
|
39
|
+
}, { readOnlyHint: true }, async ({ project_slug, status, kind, assignee_id, limit }) => {
|
|
40
40
|
const issues = await fetchProjectIssues(project_slug, { status, kind, assignee_id, limit });
|
|
41
41
|
return {
|
|
42
42
|
content: [{ type: 'text', text: toToolText(issues) }],
|
|
@@ -100,7 +100,7 @@ export function registerIssueTools(server) {
|
|
|
100
100
|
});
|
|
101
101
|
server.tool('list_issue_links', 'List GitHub PR ↔ FerrTrack issue links for one issue (created by the GitHub webhook on push).', {
|
|
102
102
|
issue_ref: z.string().min(1).describe('Issue ref, e.g. "FT-12"'),
|
|
103
|
-
}, async ({ issue_ref }) => {
|
|
103
|
+
}, { readOnlyHint: true }, async ({ issue_ref }) => {
|
|
104
104
|
const token = await getToken();
|
|
105
105
|
const links = await trackRequest(`/issues/${encodeURIComponent(issue_ref)}/links`, {
|
|
106
106
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"milestones.d.ts","sourceRoot":"","sources":["../../src/tools/milestones.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAc1E,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"milestones.d.ts","sourceRoot":"","sources":["../../src/tools/milestones.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAc1E,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QA6FvD"}
|
package/dist/tools/milestones.js
CHANGED
|
@@ -2,50 +2,41 @@ import { z } from 'zod';
|
|
|
2
2
|
import { getToken, toToolText } from '@ferrlabs/mcp-core';
|
|
3
3
|
import { trackRequest } from '../api-base.js';
|
|
4
4
|
export function registerMilestoneTools(server) {
|
|
5
|
-
server.tool('get_milestone', 'Get a single FerrTrack milestone by id.', {
|
|
6
|
-
milestone_id: z.string().min(1).describe('Milestone id'),
|
|
7
|
-
}, async ({ milestone_id }) => {
|
|
8
|
-
const token = await getToken();
|
|
9
|
-
const milestone = await trackRequest(`/milestones/${encodeURIComponent(milestone_id)}`, { token });
|
|
10
|
-
return {
|
|
11
|
-
content: [{ type: 'text', text: toToolText(milestone) }],
|
|
12
|
-
};
|
|
13
|
-
});
|
|
14
5
|
server.tool('list_milestones', 'List milestones for a FerrTrack project.', {
|
|
15
6
|
project_slug: z.string().min(1).describe('Project slug'),
|
|
16
|
-
}, async ({ project_slug }) => {
|
|
7
|
+
}, { readOnlyHint: true }, async ({ project_slug }) => {
|
|
17
8
|
const token = await getToken();
|
|
18
9
|
const milestones = await trackRequest(`/projects/${encodeURIComponent(project_slug)}/milestones`, { token });
|
|
19
10
|
return {
|
|
20
11
|
content: [{ type: 'text', text: toToolText(milestones) }],
|
|
21
12
|
};
|
|
22
13
|
});
|
|
23
|
-
server.tool('create_milestone', 'Create a milestone on a project.
|
|
14
|
+
server.tool('create_milestone', 'Create a milestone on a project. Target date is optional.', {
|
|
24
15
|
project_slug: z.string().min(1).describe('Project slug'),
|
|
25
16
|
name: z.string().min(1).max(100),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}, async ({ project_slug, name,
|
|
17
|
+
summary: z.string().max(2000).optional(),
|
|
18
|
+
target_date: z.iso.date().optional().describe('ISO date (YYYY-MM-DD).'),
|
|
19
|
+
}, async ({ project_slug, name, summary, target_date }) => {
|
|
29
20
|
const token = await getToken();
|
|
30
21
|
const milestone = await trackRequest(`/projects/${encodeURIComponent(project_slug)}/milestones`, {
|
|
31
22
|
token,
|
|
32
23
|
method: 'POST',
|
|
33
24
|
body: {
|
|
34
25
|
name,
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
summary: summary ?? null,
|
|
27
|
+
target_date: target_date ?? null,
|
|
37
28
|
},
|
|
38
29
|
});
|
|
39
30
|
return {
|
|
40
31
|
content: [{ type: 'text', text: toToolText(milestone) }],
|
|
41
32
|
};
|
|
42
33
|
});
|
|
43
|
-
server.tool('update_milestone', 'Patch a milestone — rename,
|
|
34
|
+
server.tool('update_milestone', 'Patch a milestone — rename, edit summary, shift target date, flip status.', {
|
|
44
35
|
milestone_id: z.string().min(1).describe('Milestone id'),
|
|
45
36
|
name: z.string().min(1).max(100).optional(),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
status: z.enum(['
|
|
37
|
+
summary: z.string().max(2000).nullable().optional(),
|
|
38
|
+
target_date: z.iso.date().nullable().optional(),
|
|
39
|
+
status: z.enum(['planned', 'in_progress', 'shipped', 'canceled']).optional(),
|
|
49
40
|
}, async ({ milestone_id, ...patch }) => {
|
|
50
41
|
const token = await getToken();
|
|
51
42
|
const body = {};
|
|
@@ -60,7 +51,7 @@ export function registerMilestoneTools(server) {
|
|
|
60
51
|
});
|
|
61
52
|
server.tool('delete_milestone', 'Delete a milestone. Issues that were on it become un-milestoned.', {
|
|
62
53
|
milestone_id: z.string().min(1).describe('Milestone id'),
|
|
63
|
-
}, async ({ milestone_id }) => {
|
|
54
|
+
}, { destructiveHint: true }, async ({ milestone_id }) => {
|
|
64
55
|
const token = await getToken();
|
|
65
56
|
await trackRequest(`/milestones/${encodeURIComponent(milestone_id)}`, {
|
|
66
57
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/tools/projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAG1E,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAGxD;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/tools/projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAG1E,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAGxD;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,QAuFrD"}
|
package/dist/tools/projects.js
CHANGED
|
@@ -6,7 +6,7 @@ export async function fetchProjects() {
|
|
|
6
6
|
return trackRequest('/projects', { token });
|
|
7
7
|
}
|
|
8
8
|
export function registerProjectTools(server) {
|
|
9
|
-
server.tool('list_track_projects', "List FerrTrack projects in the caller's active organization.", {}, async () => {
|
|
9
|
+
server.tool('list_track_projects', "List FerrTrack projects in the caller's active organization.", {}, { readOnlyHint: true }, async () => {
|
|
10
10
|
const projects = await fetchProjects();
|
|
11
11
|
return {
|
|
12
12
|
content: [{ type: 'text', text: toToolText(projects) }],
|
|
@@ -14,7 +14,7 @@ export function registerProjectTools(server) {
|
|
|
14
14
|
});
|
|
15
15
|
server.tool('get_project', 'Get a FerrTrack project by slug.', {
|
|
16
16
|
project_slug: z.string().min(1).describe('Project slug'),
|
|
17
|
-
}, async ({ project_slug }) => {
|
|
17
|
+
}, { readOnlyHint: true }, async ({ project_slug }) => {
|
|
18
18
|
const token = await getToken();
|
|
19
19
|
const project = await trackRequest(`/projects/${encodeURIComponent(project_slug)}`, {
|
|
20
20
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAmB1E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAc,MAAM,oBAAoB,CAAC;AAmB1E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,QAuCpD"}
|
package/dist/tools/search.js
CHANGED
|
@@ -11,7 +11,7 @@ export function registerSearchTools(server) {
|
|
|
11
11
|
.max(100)
|
|
12
12
|
.optional()
|
|
13
13
|
.describe('Max hits to return (default 25).'),
|
|
14
|
-
}, async ({ q, limit }) => {
|
|
14
|
+
}, { readOnlyHint: true }, async ({ q, limit }) => {
|
|
15
15
|
const token = await getToken();
|
|
16
16
|
const params = new URLSearchParams({ q });
|
|
17
17
|
if (limit !== undefined)
|
|
@@ -21,7 +21,7 @@ export function registerSearchTools(server) {
|
|
|
21
21
|
content: [{ type: 'text', text: toToolText(hits) }],
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
|
-
server.tool('list_track_users', "List FerrTrack users in the caller's org — useful for resolving an `assignee_id` before calling create_issue or update_issue.", {}, async () => {
|
|
24
|
+
server.tool('list_track_users', "List FerrTrack users in the caller's org — useful for resolving an `assignee_id` before calling create_issue or update_issue.", {}, { readOnlyHint: true }, async () => {
|
|
25
25
|
const token = await getToken();
|
|
26
26
|
const users = await trackRequest('/users', { token });
|
|
27
27
|
return {
|