tododdle-mcp 3.8.0 → 3.9.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +19 -4
- package/config/external-api-parity.json +1 -0
- package/dist/api-client.d.ts +8 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +24 -2
- package/dist/api-client.js.map +1 -1
- package/dist/compatibility.d.ts +2 -2
- package/dist/compatibility.js +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -8
- package/dist/config.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +21 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +199 -11
- package/dist/server.js.map +1 -1
- package/dist/upload-source.d.ts +1 -1
- package/dist/upload-source.d.ts.map +1 -1
- package/dist/upload-source.js +20 -13
- package/dist/upload-source.js.map +1 -1
- package/dist/workflow-guidance.d.ts +1 -1
- package/dist/workflow-guidance.d.ts.map +1 -1
- package/dist/workflow-guidance.js +11 -1
- package/dist/workflow-guidance.js.map +1 -1
- package/package.json +1 -1
- package/skills/tododdle-workflow/SKILL.md +2 -0
- package/skills/tododdle-workflow/references/files.md +2 -2
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { ToDoddleApiError } from './api-client.js';
|
|
5
6
|
import { prepareUploadSource } from './upload-source.js';
|
|
6
7
|
import { TODODDLE_WORKFLOW_GUIDANCE } from './workflow-guidance.js';
|
|
7
8
|
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -229,6 +230,78 @@ const assessmentEvidenceSchema = z
|
|
|
229
230
|
});
|
|
230
231
|
}
|
|
231
232
|
});
|
|
233
|
+
/**
|
|
234
|
+
* Turns a ToDoddle API failure into a tool error the agent can act on. The SDK
|
|
235
|
+
* default keeps only the message, which dropped the scope an owner must grant.
|
|
236
|
+
*/
|
|
237
|
+
export function apiErrorResult(error) {
|
|
238
|
+
const missing = error.scopes?.missingScopes ?? [];
|
|
239
|
+
const hint = error.code === 'scope_required' && missing.length
|
|
240
|
+
? `Ask a workspace owner or admin to enable ${missing.join(', ')} for this Agent Connection in Workspace Settings > Agent Connections, then retry.`
|
|
241
|
+
: error.code === 'scope_required'
|
|
242
|
+
? 'Ask a workspace owner or admin to check the scopes on this Agent Connection in Workspace Settings > Agent Connections.'
|
|
243
|
+
: undefined;
|
|
244
|
+
const body = {
|
|
245
|
+
error: error.code,
|
|
246
|
+
status: error.status,
|
|
247
|
+
message: error.message,
|
|
248
|
+
...(missing.length ? { missingScopes: missing } : {}),
|
|
249
|
+
...(error.scopes?.requiredScopes ? { requiredScopes: error.scopes.requiredScopes } : {}),
|
|
250
|
+
...(hint ? { hint } : {}),
|
|
251
|
+
...(error.details !== undefined ? { details: error.details } : {}),
|
|
252
|
+
};
|
|
253
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(body) }] };
|
|
254
|
+
}
|
|
255
|
+
const LINK_KEYS = new Set(['uiUrl', 'href']);
|
|
256
|
+
/**
|
|
257
|
+
* The API returns web links as paths such as /dashboard/project/x. Left
|
|
258
|
+
* relative, a client guesses the host and often picks www.tododdle.com, which
|
|
259
|
+
* is the marketing site. Turn them into absolute links on the app origin.
|
|
260
|
+
* In-app embed paths (/api/documents/...) are left alone.
|
|
261
|
+
*/
|
|
262
|
+
export function absolutizeLinks(value, appOrigin) {
|
|
263
|
+
if (Array.isArray(value))
|
|
264
|
+
return value.map((item) => absolutizeLinks(item, appOrigin));
|
|
265
|
+
if (!value || typeof value !== 'object')
|
|
266
|
+
return value;
|
|
267
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
268
|
+
key,
|
|
269
|
+
LINK_KEYS.has(key) &&
|
|
270
|
+
typeof item === 'string' &&
|
|
271
|
+
item.startsWith('/') &&
|
|
272
|
+
!item.startsWith('//')
|
|
273
|
+
? new URL(item, appOrigin).toString()
|
|
274
|
+
: absolutizeLinks(item, appOrigin),
|
|
275
|
+
]));
|
|
276
|
+
}
|
|
277
|
+
function withAbsoluteLinks(result, appOrigin) {
|
|
278
|
+
const typed = result;
|
|
279
|
+
if (!appOrigin || !typed?.structuredContent)
|
|
280
|
+
return result;
|
|
281
|
+
const structuredContent = absolutizeLinks(typed.structuredContent, appOrigin);
|
|
282
|
+
return {
|
|
283
|
+
...typed,
|
|
284
|
+
structuredContent,
|
|
285
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Wraps every tool so API failures keep their code, scopes, and hint, and
|
|
290
|
+
* web links in results are absolute.
|
|
291
|
+
*/
|
|
292
|
+
function withApiErrorResults(server, appOrigin) {
|
|
293
|
+
const register = server.registerTool.bind(server);
|
|
294
|
+
server.registerTool = (name, config, callback) => register(name, config, (async (...args) => {
|
|
295
|
+
try {
|
|
296
|
+
return withAbsoluteLinks(await callback(...args), appOrigin);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error instanceof ToDoddleApiError)
|
|
300
|
+
return apiErrorResult(error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
232
305
|
function toolResult(value) {
|
|
233
306
|
return {
|
|
234
307
|
// Keep text content for clients that do not consume structuredContent, but
|
|
@@ -338,8 +411,35 @@ const completeUploadInputSchema = z.object({
|
|
|
338
411
|
error: z.string().max(1000).optional(),
|
|
339
412
|
idempotencyKey: z.string().min(8),
|
|
340
413
|
});
|
|
414
|
+
const EMBEDDABLE_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
415
|
+
/** Escapes a file name for use as Markdown image alt text. */
|
|
416
|
+
function markdownAlt(value) {
|
|
417
|
+
return value.replace(/[\\[\]]/g, '\\$&');
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Adds what an agent needs next: `embedMarkdown` to show an image inline in a
|
|
421
|
+
* Note or Context, and a `notice` when the stored type differs. ToDoddle stores
|
|
422
|
+
* SVG, HTML, and other active content as an opaque file so it never runs in a
|
|
423
|
+
* browser; an SVG previews and embeds through a PNG rendering instead.
|
|
424
|
+
*/
|
|
425
|
+
function withUploadHints(result, requestedType, fileName) {
|
|
426
|
+
const entity = asRecord(result.entity);
|
|
427
|
+
const storedType = entity?.mimeType;
|
|
428
|
+
const id = typeof entity?.id === 'string' ? entity.id : undefined;
|
|
429
|
+
const isSvg = /\.svg$/i.test(fileName);
|
|
430
|
+
const hints = {};
|
|
431
|
+
if (id && typeof storedType === 'string' && (EMBEDDABLE_IMAGE_TYPES.has(storedType) || isSvg)) {
|
|
432
|
+
hints.embedMarkdown = ``;
|
|
433
|
+
}
|
|
434
|
+
if (typeof storedType === 'string' && storedType !== requestedType) {
|
|
435
|
+
hints.notice = isSvg
|
|
436
|
+
? `Stored as ${storedType}: ToDoddle keeps SVG files as opaque downloads for safety. Previews and embeds use a PNG rendering of it, so embedMarkdown still shows the image.`
|
|
437
|
+
: `Stored as ${storedType}, not ${requestedType}. ToDoddle stores HTML and other active content as an opaque file for safety. It downloads normally but does not preview inline.`;
|
|
438
|
+
}
|
|
439
|
+
return { ...result, ...hints };
|
|
440
|
+
}
|
|
341
441
|
async function uploadDocument(api, options, input) {
|
|
342
|
-
const prepared = await prepareUploadSource(input, options.
|
|
442
|
+
const prepared = await prepareUploadSource(input, options.maxUploadBytes, options.managedUploadRoot);
|
|
343
443
|
let session = null;
|
|
344
444
|
let bytesUploaded = false;
|
|
345
445
|
let uploadConfirmed = false;
|
|
@@ -357,7 +457,7 @@ async function uploadDocument(api, options, input) {
|
|
|
357
457
|
bytesUploaded = true;
|
|
358
458
|
const result = await api.post(`/api/external/projects/${input.projectId}/documents/${session.documentId}/finalize`, { success: true, taskId: input.taskId }, `${idempotencyKey}-finalize`);
|
|
359
459
|
uploadConfirmed = true;
|
|
360
|
-
return result;
|
|
460
|
+
return withUploadHints(result, prepared.contentType, prepared.fileName);
|
|
361
461
|
}
|
|
362
462
|
catch (error) {
|
|
363
463
|
if (session && !bytesUploaded) {
|
|
@@ -426,11 +526,12 @@ const securityEventTypes = [
|
|
|
426
526
|
];
|
|
427
527
|
export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
428
528
|
const options = {
|
|
429
|
-
uploadRoots: serverOptions.uploadRoots ?? [],
|
|
430
529
|
managedUploadRoot: serverOptions.managedUploadRoot,
|
|
530
|
+
appOrigin: serverOptions.appOrigin,
|
|
431
531
|
maxUploadBytes: serverOptions.maxUploadBytes ?? 1024 * 1024 * 1024,
|
|
432
532
|
};
|
|
433
533
|
const server = new McpServer({ name: 'tododdle', version: packageVersion.version }, { instructions: TODODDLE_WORKFLOW_GUIDANCE });
|
|
534
|
+
withApiErrorResults(server, options.appOrigin);
|
|
434
535
|
server.registerTool('list_projects', {
|
|
435
536
|
description: 'List projects accessible through this Agent Connection.',
|
|
436
537
|
inputSchema: z.object({
|
|
@@ -1177,7 +1278,7 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1177
1278
|
},
|
|
1178
1279
|
}, async ({ projectId, noteId }) => toolResult(await api.get(`/api/external/notes/${noteId}`, { projectId })));
|
|
1179
1280
|
server.registerTool('create_note', {
|
|
1180
|
-
description: 'Create a workspace or project Note. Supply projectId for a project Note.',
|
|
1281
|
+
description: 'Create a workspace or project Note. Supply projectId for a project Note. To show an uploaded image inline, paste the embedMarkdown returned by upload_project_document or complete_upload, for example ; it shows only to people who can open that document.',
|
|
1181
1282
|
inputSchema: z.object({
|
|
1182
1283
|
projectId: z.string().min(1).optional(),
|
|
1183
1284
|
title: z.string().min(1).max(500),
|
|
@@ -1193,7 +1294,7 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1193
1294
|
},
|
|
1194
1295
|
}, async ({ idempotencyKey, ...body }) => toolResult(await api.post('/api/external/notes', body, idempotencyKey)));
|
|
1195
1296
|
server.registerTool('update_note', {
|
|
1196
|
-
description: 'Update a workspace or project Note using optimistic concurrency.',
|
|
1297
|
+
description: 'Update a workspace or project Note using optimistic concurrency. To show an uploaded image inline, paste the embedMarkdown returned by upload_project_document or complete_upload, for example ; it shows only to people who can open that document.',
|
|
1197
1298
|
inputSchema: z.object({
|
|
1198
1299
|
projectId: z.string().min(1).optional(),
|
|
1199
1300
|
noteId: z.string().min(1),
|
|
@@ -1263,9 +1364,16 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1263
1364
|
idempotentHint: true,
|
|
1264
1365
|
openWorldHint: false,
|
|
1265
1366
|
},
|
|
1266
|
-
}, async ({ projectId, documentId, idempotencyKey, ...body }) =>
|
|
1367
|
+
}, async ({ projectId, documentId, idempotencyKey, ...body }) => {
|
|
1368
|
+
const result = await api.post(`/api/external/projects/${projectId}/documents/${documentId}/finalize`, body, idempotencyKey);
|
|
1369
|
+
// The remote client sent the bytes, so the stored type is the only type to compare.
|
|
1370
|
+
const entity = asRecord(result.entity);
|
|
1371
|
+
const fileName = typeof entity?.fileName === 'string' ? entity.fileName : '';
|
|
1372
|
+
const storedType = typeof entity?.mimeType === 'string' ? entity.mimeType : '';
|
|
1373
|
+
return toolResult(body.success === false ? result : withUploadHints(result, storedType, fileName));
|
|
1374
|
+
});
|
|
1267
1375
|
server.registerTool('upload_project_document', {
|
|
1268
|
-
description: 'Upload an approved local file or HTTPS URL to a project document library. Local paths must be regular files in the system temporary directory
|
|
1376
|
+
description: 'Upload an approved local file or HTTPS URL to a project document library. Local paths must be regular files in the system temporary directory, owned by the current user; copy a file there first if needed. SVG and HTML are stored as opaque files that download but do not preview.',
|
|
1269
1377
|
inputSchema: uploadInputSchema,
|
|
1270
1378
|
annotations: {
|
|
1271
1379
|
readOnlyHint: false,
|
|
@@ -1275,7 +1383,7 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1275
1383
|
},
|
|
1276
1384
|
}, async (input) => toolResult(await uploadDocument(api, options, input)));
|
|
1277
1385
|
server.registerTool('attach_file_to_ticket', {
|
|
1278
|
-
description: 'Upload a local file or HTTPS URL and attach it to a ticket in the same project. Local paths must be regular files in the system temporary directory
|
|
1386
|
+
description: 'Upload a local file or HTTPS URL and attach it to a ticket in the same project. Local paths must be regular files in the system temporary directory, owned by the current user; copy a file there first if needed. SVG and HTML are stored as opaque files that download but do not preview.',
|
|
1279
1387
|
inputSchema: taskUploadInputSchema,
|
|
1280
1388
|
annotations: {
|
|
1281
1389
|
readOnlyHint: false,
|
|
@@ -1394,7 +1502,7 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1394
1502
|
},
|
|
1395
1503
|
}, async ({ taskId }) => toolResult(await api.delete(`/api/external/focus/${taskId}`, {})));
|
|
1396
1504
|
server.registerTool('search_project', {
|
|
1397
|
-
description: 'Find compact ticket, board, lane, Note, Context, and document matches within one known project. Use this for discovery before narrow get tools instead of scanning every board or lane.',
|
|
1505
|
+
description: 'Find compact ticket, board, lane, Note, Context, and document matches within one known project. Use this for discovery before narrow get tools instead of scanning every board or lane. Each result has relevance (0 to 1; 0.8 or more is an exact or phrase title match) and matchedTerms. Matching uses titles only; an empty result means no title matched well.',
|
|
1398
1506
|
inputSchema: z.object({
|
|
1399
1507
|
projectId: z.string().min(1),
|
|
1400
1508
|
query: z.string().trim().min(2).max(200),
|
|
@@ -1756,6 +1864,49 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
1756
1864
|
openWorldHint: false,
|
|
1757
1865
|
},
|
|
1758
1866
|
}, async ({ taskId, runId, projectId, ...body }) => toolResult(await api.post(`/api/external/tasks/${taskId}/agent-runs/${runId}`, body, undefined, runContext(runId, projectId, taskId, 'RECORD_EVIDENCE'))));
|
|
1867
|
+
server.registerTool('log_completed_work', {
|
|
1868
|
+
description: 'Record finished ad-hoc work in one call, for quick requests done in conversation without claiming a ticket first. ' +
|
|
1869
|
+
'Pass taskId for an existing ticket, or ticket (planId, sectionId, title) to create one. ToDoddle claims it, sets the status, and records a finished Agent Run with your outcome and evidence (commits, tests, files, links), so it shows in run history like claimed work. ' +
|
|
1870
|
+
'status defaults to COMPLETE. A ticket another person created goes to REVIEW instead; the response warns you, and you should then call create_review_request. ' +
|
|
1871
|
+
'Use claim_ticket and finish_agent_run for planned work. Reuse the same idempotencyKey to retry safely.',
|
|
1872
|
+
inputSchema: z.object({
|
|
1873
|
+
projectId: z.string().min(1),
|
|
1874
|
+
taskId: z.string().min(1).optional(),
|
|
1875
|
+
ticket: z
|
|
1876
|
+
.object({
|
|
1877
|
+
planId: z.string().min(1),
|
|
1878
|
+
sectionId: z.string().min(1),
|
|
1879
|
+
title: z.string().trim().min(1).max(500),
|
|
1880
|
+
description: z.string().trim().max(50_000).optional(),
|
|
1881
|
+
kind: taskKindSchema.optional(),
|
|
1882
|
+
priority: prioritySchema.optional(),
|
|
1883
|
+
})
|
|
1884
|
+
.optional(),
|
|
1885
|
+
runId: z.string().min(1).max(200).optional(),
|
|
1886
|
+
outcome: z.string().trim().min(1).max(2000),
|
|
1887
|
+
evidence: z
|
|
1888
|
+
.array(z.object({
|
|
1889
|
+
type: agentRunEvidenceTypeSchema,
|
|
1890
|
+
reference: z.string().trim().min(1).max(2000),
|
|
1891
|
+
label: z.string().trim().min(1).max(200).optional(),
|
|
1892
|
+
}))
|
|
1893
|
+
.max(25)
|
|
1894
|
+
.default([]),
|
|
1895
|
+
status: z.enum(['COMPLETE', 'REVIEW']).default('COMPLETE'),
|
|
1896
|
+
idempotencyKey: z.string().trim().min(8).max(180),
|
|
1897
|
+
}),
|
|
1898
|
+
annotations: {
|
|
1899
|
+
readOnlyHint: false,
|
|
1900
|
+
destructiveHint: false,
|
|
1901
|
+
idempotentHint: true,
|
|
1902
|
+
openWorldHint: false,
|
|
1903
|
+
},
|
|
1904
|
+
}, async ({ projectId, ...body }) => {
|
|
1905
|
+
if (Boolean(body.taskId) === Boolean(body.ticket)) {
|
|
1906
|
+
throw new Error('Send taskId for an existing ticket or ticket to create one, not both.');
|
|
1907
|
+
}
|
|
1908
|
+
return toolResult(await api.post(`/api/external/projects/${projectId}/completed-work`, body));
|
|
1909
|
+
});
|
|
1759
1910
|
server.registerTool('get_supervisor_state', {
|
|
1760
1911
|
description: 'Get compact, permission-filtered task state for an external decision or supervision provider. The response contains evidence and policy hints, but grants no authority to change work state.',
|
|
1761
1912
|
inputSchema: z.object({
|
|
@@ -2228,7 +2379,10 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
2228
2379
|
},
|
|
2229
2380
|
}, async ({ projectId, artifactId, idempotencyKey, ...body }) => toolResult(await api.post(`/api/external/projects/${projectId}/artifacts/${artifactId}/relations`, body, idempotencyKey)));
|
|
2230
2381
|
server.registerTool('update_project_artifact', {
|
|
2231
|
-
description: 'Update artifact Markdown, its current short summary, or metadata using optimistic concurrency. Editing approved content returns it to draft.'
|
|
2382
|
+
description: 'Update artifact Markdown, its current short summary, or metadata using optimistic concurrency. Editing approved content returns it to draft. ' +
|
|
2383
|
+
'To change part of a long document, send contentEdits instead of content: replace_section (by heading path, e.g. "Release Plan" or "Plan > Risks"; replaces the text under that heading and keeps its subsections), append_section (optionally inside a parent heading), or find_replace (exact text; expectedCount defaults to 1). ' +
|
|
2384
|
+
'Pass baseVersion (the version you read) so an edit still applies when someone else changed a different section; the server rejects it if your section changed. ' +
|
|
2385
|
+
'The response is a slim summary (id, version, updatedAt, changedFields) unless verbose is true. Embed an uploaded image with the embedMarkdown returned by the upload tools.',
|
|
2232
2386
|
inputSchema: z.object({
|
|
2233
2387
|
projectId: z.string().min(1),
|
|
2234
2388
|
artifactId: z.string().min(1),
|
|
@@ -2236,10 +2390,36 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
2236
2390
|
title: z.string().min(1).optional(),
|
|
2237
2391
|
summary: z.string().trim().max(500).nullable().optional(),
|
|
2238
2392
|
content: z.string().optional(),
|
|
2393
|
+
contentEdits: z
|
|
2394
|
+
.array(z.discriminatedUnion('op', [
|
|
2395
|
+
z.object({
|
|
2396
|
+
op: z.literal('replace_section'),
|
|
2397
|
+
heading: z.string().min(1).max(500),
|
|
2398
|
+
content: z.string(),
|
|
2399
|
+
}),
|
|
2400
|
+
z.object({
|
|
2401
|
+
op: z.literal('append_section'),
|
|
2402
|
+
heading: z.string().min(1).max(200),
|
|
2403
|
+
content: z.string(),
|
|
2404
|
+
parent: z.string().min(1).max(500).optional(),
|
|
2405
|
+
level: z.number().int().min(1).max(6).optional(),
|
|
2406
|
+
}),
|
|
2407
|
+
z.object({
|
|
2408
|
+
op: z.literal('find_replace'),
|
|
2409
|
+
find: z.string().min(1),
|
|
2410
|
+
replace: z.string(),
|
|
2411
|
+
expectedCount: z.number().int().min(1).max(1000).optional(),
|
|
2412
|
+
}),
|
|
2413
|
+
]))
|
|
2414
|
+
.min(1)
|
|
2415
|
+
.max(20)
|
|
2416
|
+
.optional(),
|
|
2417
|
+
baseVersion: z.number().int().min(1).optional(),
|
|
2239
2418
|
parentArtifactId: z.string().min(1).nullable().optional(),
|
|
2240
2419
|
ownerId: z.string().optional(),
|
|
2241
2420
|
metadata: z.record(z.unknown()).optional(),
|
|
2242
2421
|
changeNote: z.string().optional(),
|
|
2422
|
+
verbose: z.boolean().default(false),
|
|
2243
2423
|
}),
|
|
2244
2424
|
annotations: {
|
|
2245
2425
|
readOnlyHint: false,
|
|
@@ -2247,7 +2427,15 @@ export function createToDoddleMcpServer(api, serverOptions = {}) {
|
|
|
2247
2427
|
idempotentHint: true,
|
|
2248
2428
|
openWorldHint: false,
|
|
2249
2429
|
},
|
|
2250
|
-
}, async ({ projectId, artifactId, ...body }) =>
|
|
2430
|
+
}, async ({ projectId, artifactId, verbose, ...body }) => {
|
|
2431
|
+
const result = await api.patch(`/api/external/projects/${projectId}/artifacts/${artifactId}${verbose ? '' : '?detail=summary'}`, body);
|
|
2432
|
+
// An older ToDoddle server strips contentEdits it does not know and reports success.
|
|
2433
|
+
const changedFields = result.changedFields;
|
|
2434
|
+
if (body.contentEdits && Array.isArray(changedFields) && !changedFields.includes('content')) {
|
|
2435
|
+
throw new Error('This ToDoddle server does not support contentEdits yet. Send the full content instead.');
|
|
2436
|
+
}
|
|
2437
|
+
return toolResult(result);
|
|
2438
|
+
});
|
|
2251
2439
|
server.registerTool('transition_project_artifact', {
|
|
2252
2440
|
description: 'Submit, approve, supersede, archive, or return a project artifact to draft.',
|
|
2253
2441
|
inputSchema: z.object({
|