@taskforcehq/taskforce 0.3.324 → 0.3.326
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/Taskforce.module.css +39 -0
- package/dist/components/features/TaskSettings.js +26 -9
- package/dist/components/task/TaskKanban.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +22 -22
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -2
- package/dist/components/views/panels/PlanningDrawer.js +26 -12
- package/dist/components/views/planningScope.d.ts +4 -0
- package/dist/components/views/planningScope.js +5 -0
- package/dist/core/AiProfileService.js +6 -1
- package/dist/hooks/useTaskforce.js +74 -17
- package/dist/hooks/workspace/workspaceLocalState.d.ts +25 -0
- package/dist/hooks/workspace/workspaceLocalState.js +38 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +0 -10
- package/dist/mcp/clientIntegrations.d.ts +1 -1
- package/dist/mcp/clientIntegrations.js +50 -0
- package/dist/mcp/clientProfileDefaults.js +4 -0
- package/dist/mcp/collaborationRegistrar.js +3 -3
- package/dist/mcp/documentAssetRegistrar.js +24 -72
- package/dist/mcp/documentHelpers.d.ts +11 -4
- package/dist/mcp/documentHelpers.js +21 -17
- package/dist/mcp/runtime.d.ts +4 -0
- package/dist/mcp/runtime.js +466 -534
- package/dist/mcp/taskPlanningRegistrar.js +45 -81
- package/dist/mcp/toolProfiles.d.ts +7 -0
- package/dist/mcp/toolProfiles.js +36 -0
- package/dist/mcp/toolRegistry.d.ts +87 -609
- package/dist/mcp/toolRegistry.js +93 -88
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
- package/dist/resources/templates/workflows/collaborate.yaml +2 -3
- package/dist/resources/templates/workflows/do.yaml +2 -3
- package/dist/resources/templates/workflows/evaluate.yaml +2 -3
- package/dist/resources/templates/workflows/execute.yaml +12 -15
- package/dist/resources/templates/workflows/plan.yaml +3 -4
- package/dist/resources/templates/workflows/review.yaml +2 -3
- package/dist/shared/taxonomyLibrary.js +1 -1
- package/dist/sync/cloudSyncApi.js +22 -3
- package/dist/types.js +1 -1
- package/dist/ui/assets/{AgentsModule-DvkbUHi0.js → AgentsModule-C4LcqhcZ.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jhOhxqHT.js → AnnotatedAttachmentWorkspace-B-Dq22d2.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-BAmEyLHL.js → ContextAttachmentManager-DsXPTvi-.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-X-CpD5OO.js → DocumentWorkspace-CxA1wO3U.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-DD33onAC.js → EntityActivityTimeline-B8nSe8l3.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-5nDXtIjf.js → InitiativesModule-VY95v38E.js} +1 -1
- package/dist/ui/assets/{PlansPage-B8nDR1hQ.js → PlansPage-CQbKDv1a.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-DrGrXJHm.js → TaskContextUpload-DvDlNpHC.js} +1 -1
- package/dist/ui/assets/TaskSettings-CUWbYYtP.js +12 -0
- package/dist/ui/assets/{WorkflowsModule-DWCGhEWT.js → WorkflowsModule-CgM3Jako.js} +1 -1
- package/dist/ui/assets/documentReferences-Do9YjrS3.js +1 -0
- package/dist/ui/assets/index--mh3pktj.js +5 -0
- package/dist/ui/assets/index-BawaaEXS.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/utils/theme.js +1 -1
- package/package.json +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
- package/src/resources/templates/workflows/collaborate.yaml +2 -3
- package/src/resources/templates/workflows/do.yaml +2 -3
- package/src/resources/templates/workflows/evaluate.yaml +2 -3
- package/src/resources/templates/workflows/execute.yaml +12 -15
- package/src/resources/templates/workflows/plan.yaml +3 -4
- package/src/resources/templates/workflows/review.yaml +2 -3
- package/dist/ui/assets/TaskSettings-BXVvZZwH.js +0 -9
- package/dist/ui/assets/documentReferences-Crup321p.js +0 -1
- package/dist/ui/assets/index-B9k24Aym.js +0 -5
- package/dist/ui/assets/index-DNeR5dYW.css +0 -1
package/dist/mcp/runtime.js
CHANGED
|
@@ -28,7 +28,8 @@ import { resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
|
|
|
28
28
|
import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
|
|
29
29
|
import { parseAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
|
|
30
30
|
import { formatInitiativeReference, formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
|
|
31
|
-
import { MCP_TOOL_PERMISSIONS, MCP_WRITE_TOOL_NAMES } from './toolRegistry.js';
|
|
31
|
+
import { MCP_TOOL_PERMISSIONS, MCP_TOOL_REGISTRY, MCP_WRITE_TOOL_NAMES } from './toolRegistry.js';
|
|
32
|
+
import { getDefaultMcpToolProfile, getMcpToolRequiredProfiles, getMcpToolSuccessor, isMcpToolVisibleInProfile, normalizeMcpToolProfile, } from './toolProfiles.js';
|
|
32
33
|
import { registerAdminWorkspaceTools } from './adminWorkspaceRegistrar.js';
|
|
33
34
|
import { registerCollaborationTools } from './collaborationRegistrar.js';
|
|
34
35
|
import { registerDocumentAssetTools } from './documentAssetRegistrar.js';
|
|
@@ -51,6 +52,21 @@ function buildStdioAuthFromEnv() {
|
|
|
51
52
|
aiProfileSeatScope: parseAiProfileSeatScope(readEnvValue('TASKFORCE_AI_SEAT_SCOPE')),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
class ToolProfileHiddenError extends Error {
|
|
56
|
+
code = 'TOOL_PROFILE_HIDDEN';
|
|
57
|
+
activeProfile;
|
|
58
|
+
requiredProfiles;
|
|
59
|
+
suggestedTool;
|
|
60
|
+
constructor(name, activeProfile) {
|
|
61
|
+
const requiredProfiles = getMcpToolRequiredProfiles(name);
|
|
62
|
+
const suggestedTool = getMcpToolSuccessor(name);
|
|
63
|
+
const suffix = suggestedTool ? ` Use ${suggestedTool.name} instead.` : ` Request one of these profiles to use it: ${requiredProfiles.join(', ')}.`;
|
|
64
|
+
super(`Tool ${name} is not available in the ${activeProfile} MCP tool profile.${suffix}`);
|
|
65
|
+
this.activeProfile = activeProfile;
|
|
66
|
+
this.requiredProfiles = requiredProfiles;
|
|
67
|
+
this.suggestedTool = suggestedTool;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
export function isMcpWriteToolName(name) {
|
|
55
71
|
return MCP_WRITE_TOOL_NAMES.has(String(name || '').trim());
|
|
56
72
|
}
|
|
@@ -154,6 +170,21 @@ function resolveRecoverableToolError(error) {
|
|
|
154
170
|
const code = error instanceof TaskforceRuleError ? error.code : undefined;
|
|
155
171
|
const statusCode = error instanceof TaskforceRuleError ? error.statusCode : undefined;
|
|
156
172
|
const details = error instanceof TaskforceRuleError ? redactRecoverableErrorDetails(error.details) : undefined;
|
|
173
|
+
if (error instanceof ToolProfileHiddenError) {
|
|
174
|
+
return {
|
|
175
|
+
message,
|
|
176
|
+
code: error.code,
|
|
177
|
+
details: {
|
|
178
|
+
activeProfile: error.activeProfile,
|
|
179
|
+
requiredProfiles: error.requiredProfiles,
|
|
180
|
+
...(error.suggestedTool ? { suggestedTool: error.suggestedTool } : {}),
|
|
181
|
+
},
|
|
182
|
+
...(error.suggestedTool ? {
|
|
183
|
+
nextTool: error.suggestedTool.name,
|
|
184
|
+
suggestedArgs: error.suggestedTool.argumentHint || {},
|
|
185
|
+
} : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
157
188
|
if (/^This tool requires an AI profile\./i.test(message)) {
|
|
158
189
|
return {
|
|
159
190
|
message,
|
|
@@ -272,6 +303,13 @@ function classifyTransientMcpError(error) {
|
|
|
272
303
|
retryHint: 'The tool transport failed before Taskforce could return a stable tool response. Retry the same read once; if it repeats, inspect the cloud MCP route logs with this requestId.'
|
|
273
304
|
};
|
|
274
305
|
}
|
|
306
|
+
if (/(EOF while parsing|Unexpected end of JSON|SyntaxError.*JSON|Unexpected token .*JSON|invalid json response body|non-JSON response|text\/plain|502 Bad Gateway|522 Connection Timed Out|bad gateway|upstream connect error)/i.test(combined)) {
|
|
307
|
+
return {
|
|
308
|
+
kind: 'transport_failure',
|
|
309
|
+
code: 'MCP_TRANSPORT_FAILURE',
|
|
310
|
+
retryHint: 'The tool transport received an empty or non-JSON response, likely from a gateway interruption. Retry the same read once; if it repeats, inspect the cloud MCP route logs with this requestId.',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
275
313
|
if (/(timed out|timeout|ETIMEDOUT|query canceled|statement timeout|database timeout|postgres worker)/i.test(combined)) {
|
|
276
314
|
return {
|
|
277
315
|
kind: 'backend_timeout',
|
|
@@ -393,12 +431,16 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
393
431
|
const PROJECT_NAME = path.basename(PROJECT_ROOT);
|
|
394
432
|
const logStartup = options.logStartup !== false;
|
|
395
433
|
const runtimeMode = options.runtimeMode === 'cloud' ? 'cloud' : 'local';
|
|
434
|
+
const defaultToolProfile = getDefaultMcpToolProfile(runtimeMode);
|
|
435
|
+
const envToolProfile = runtimeMode === 'local' ? readEnvValue('TASKFORCE_MCP_TOOL_PROFILE') : null;
|
|
436
|
+
const activeToolProfile = normalizeMcpToolProfile(options.toolProfile ?? envToolProfile, defaultToolProfile);
|
|
396
437
|
const allowedScopes = Array.isArray(options.auth?.scopes)
|
|
397
438
|
? Array.from(new Set(options.auth.scopes))
|
|
398
439
|
: null;
|
|
399
440
|
const configuredAiProfileSeatScope = runtimeMode === 'cloud' ? 'cloud_metered' : 'local_unmetered';
|
|
400
441
|
if (logStartup) {
|
|
401
442
|
console.error(`[Taskforce MCP] Starting for project: ${PROJECT_NAME} at ${PROJECT_ROOT}`);
|
|
443
|
+
console.error(`[Taskforce MCP] Tool profile: ${activeToolProfile}`);
|
|
402
444
|
}
|
|
403
445
|
const tenantId = String(options.tenantId || options.core?.getTenantId() || process.env.TASKFORCE_TENANT_ID || 'default').trim() || 'default';
|
|
404
446
|
const MCP_SERVER_VERSION = (() => {
|
|
@@ -505,7 +547,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
505
547
|
},
|
|
506
548
|
tools: {},
|
|
507
549
|
},
|
|
508
|
-
instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: Before mutating Taskforce data, ensure Taskforce knows which AI profile you are acting as. The preferred path is ambient session binding: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes. If no AI identity is currently bound, call resolve_profile with your name. Reuse the returned profileToken across sessions, especially for stateless clients.\n\nStateless client pattern: call resolve_profile, save the returned profileToken only in private agent/client state, and include profileToken on later mutating tool calls only when your MCP client does not preserve session identity between calls. Never write profileToken values to repos, task comments, task attachments, logs, generated docs, or shared output.\n\nTo inspect your current attribution state before writes, call get_current_profile. It reports whether identity is bound, how it was resolved, and the suggested args to use with list_tasks.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, cli for terminal-native coding agents, coding_tool for coding surfaces that are not specifically CLI or IDE-native, chat_app for a conversational app identity, and ide for an IDE-native copilot identity. If you are unsure or none of those fit, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use
|
|
550
|
+
instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: Before mutating Taskforce data, ensure Taskforce knows which AI profile you are acting as. The preferred path is ambient session binding: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes. If no AI identity is currently bound, call resolve_profile with your name. Reuse the returned profileToken across sessions, especially for stateless clients.\n\nStateless client pattern: call resolve_profile, save the returned profileToken only in private agent/client state, and include profileToken on later mutating tool calls only when your MCP client does not preserve session identity between calls. Never write profileToken values to repos, task comments, task attachments, logs, generated docs, or shared output.\n\nTo inspect your current attribution state before writes, call get_current_profile. It reports whether identity is bound, how it was resolved, and the suggested args to use with list_tasks.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, cli for terminal-native coding agents, coding_tool for coding surfaces that are not specifically CLI or IDE-native, chat_app for a conversational app identity, and ide for an IDE-native copilot identity. If you are unsure or none of those fit, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use get_image_annotations to list saved sessions or read a structured annotation payload. If an image reference is being used for UI, placement, or visual correction feedback, treat the saved annotation payload as the source of truth and do not infer intent from the screenshot alone.\n\nFor documents, use search_documents first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_document; pass includeContent=true when you need the document body. For a lower-friction review flow, prefer review_document; otherwise use get_document_reviews or create_document_review_session, then add_document_review_comment to record review feedback.`,
|
|
509
551
|
});
|
|
510
552
|
const TOOL_PERMISSIONS = MCP_TOOL_PERMISSIONS;
|
|
511
553
|
const RESOURCE_PERMISSIONS = {
|
|
@@ -532,6 +574,13 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
532
574
|
return;
|
|
533
575
|
throw new Error(`Tool ${name} is not available for this MCP session.`);
|
|
534
576
|
}
|
|
577
|
+
function assertToolProfile(name) {
|
|
578
|
+
if (!MCP_TOOL_REGISTRY[name])
|
|
579
|
+
return;
|
|
580
|
+
if (isMcpToolVisibleInProfile(name, activeToolProfile))
|
|
581
|
+
return;
|
|
582
|
+
throw new ToolProfileHiddenError(name, activeToolProfile);
|
|
583
|
+
}
|
|
535
584
|
function resolveResourcePermissionKey(uri) {
|
|
536
585
|
if (uri === RESOURCE_CONFIG_URI)
|
|
537
586
|
return 'config';
|
|
@@ -590,6 +639,45 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
590
639
|
const buildDocumentDescriptor = documentHelpers.buildDocumentDescriptor;
|
|
591
640
|
const buildDocumentReadHint = documentHelpers.buildDocumentReadHint;
|
|
592
641
|
const buildCanonicalDocumentAttachmentHint = documentHelpers.buildCanonicalDocumentAttachmentHint;
|
|
642
|
+
async function buildDocumentContentPayload(asset, options) {
|
|
643
|
+
const document = buildDocumentDescriptor(asset);
|
|
644
|
+
const formatRaw = typeof options?.format === 'string' ? options.format.trim().toLowerCase() : '';
|
|
645
|
+
const format = formatRaw === 'content' || formatRaw === 'full' ? 'text' : formatRaw;
|
|
646
|
+
if (format === "metadata") {
|
|
647
|
+
return {
|
|
648
|
+
document,
|
|
649
|
+
format: "metadata"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (format === "download") {
|
|
653
|
+
return {
|
|
654
|
+
document,
|
|
655
|
+
format: "download",
|
|
656
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
657
|
+
reason: "requested_download"
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (!isTextReadableMimeType(asset.mimeType || '')) {
|
|
661
|
+
return {
|
|
662
|
+
document,
|
|
663
|
+
format: "download",
|
|
664
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
665
|
+
reason: "binary_file"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const resolvedMaxChars = Math.max(1, Math.floor(Number(options?.maxChars || 200000)));
|
|
669
|
+
const buffer = await canonicalAssetHelpers.readCanonicalDocumentBuffer(asset);
|
|
670
|
+
const content = buffer.toString('utf8');
|
|
671
|
+
if (content.length > resolvedMaxChars) {
|
|
672
|
+
throw new Error('Document exceeds maxChars limit for inline read');
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
document,
|
|
676
|
+
format: "text",
|
|
677
|
+
truncated: false,
|
|
678
|
+
content
|
|
679
|
+
};
|
|
680
|
+
}
|
|
593
681
|
function searchImageAssets(queryRaw, options) {
|
|
594
682
|
const query = typeof queryRaw === 'string' ? queryRaw.trim().toLowerCase() : '';
|
|
595
683
|
if (!query) {
|
|
@@ -773,12 +861,59 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
773
861
|
lastActiveAt: profile.lastActiveAt ?? null,
|
|
774
862
|
};
|
|
775
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Build the common attribution payload used by both resolve_profile and get_current_profile.
|
|
866
|
+
* When profile is null, bindingSource is always 'none' (the parameter is ignored in the null path).
|
|
867
|
+
*/
|
|
868
|
+
function buildAiProfileAttribution(profile, bindingSource) {
|
|
869
|
+
if (!profile) {
|
|
870
|
+
return {
|
|
871
|
+
identityBound: false,
|
|
872
|
+
bindingSource: 'none',
|
|
873
|
+
writeReady: false,
|
|
874
|
+
profileToken: null,
|
|
875
|
+
profile: null,
|
|
876
|
+
taskFilter: null,
|
|
877
|
+
nextTool: 'resolve_profile',
|
|
878
|
+
suggestedArgs: { name: 'Your AI name' },
|
|
879
|
+
guidance: "No AI profile is currently bound. Call resolve_profile first, then use the returned profile id as the assignee filter for list_tasks."
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
identityBound: true,
|
|
884
|
+
bindingSource,
|
|
885
|
+
writeReady: true,
|
|
886
|
+
profileToken: profile.profileToken ?? null,
|
|
887
|
+
profile: buildAiProfileDescriptor(profile),
|
|
888
|
+
taskFilter: { assignee: profile.id },
|
|
889
|
+
nextTool: 'list_tasks',
|
|
890
|
+
suggestedArgs: { assignee: profile.id, format: 'json' },
|
|
891
|
+
guidance: "Use suggestedArgs with list_tasks to get tasks assigned to this AI profile."
|
|
892
|
+
};
|
|
893
|
+
}
|
|
776
894
|
function resolveRuntimeTaskAssigneeInput(value) {
|
|
895
|
+
if (isCurrentAiAssigneeInput(value)) {
|
|
896
|
+
const currentAiAssignee = String(activeAiProfileId || '').trim();
|
|
897
|
+
if (!currentAiAssignee) {
|
|
898
|
+
throw new Error('This tool requires an AI profile. Call resolve_profile first, then retry.');
|
|
899
|
+
}
|
|
900
|
+
return currentAiAssignee;
|
|
901
|
+
}
|
|
777
902
|
const resolved = resolveTaskAssigneeInput(value);
|
|
778
903
|
if (resolved === 'agent')
|
|
779
904
|
return 'unassigned';
|
|
780
905
|
return resolved;
|
|
781
906
|
}
|
|
907
|
+
function resolveListTasksAssigneeInput(value) {
|
|
908
|
+
if (!isCurrentAiAssigneeInput(value))
|
|
909
|
+
return value;
|
|
910
|
+
ensureResolvedProfileForSessionBootstrap();
|
|
911
|
+
const currentAiAssignee = String(activeAiProfileId || '').trim();
|
|
912
|
+
if (!currentAiAssignee) {
|
|
913
|
+
throw new Error('This tool requires an AI profile. Call get_current_profile or resolve_profile first, then retry.');
|
|
914
|
+
}
|
|
915
|
+
return currentAiAssignee;
|
|
916
|
+
}
|
|
782
917
|
function resolvePlanningOwnerInput(value) {
|
|
783
918
|
const resolved = resolveRuntimeTaskAssigneeInput(value);
|
|
784
919
|
if (resolved === undefined)
|
|
@@ -1294,7 +1429,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1294
1429
|
function logMcpReadObservation(toolName, startedAtMs, details = {}) {
|
|
1295
1430
|
const durationMs = Date.now() - startedAtMs;
|
|
1296
1431
|
const shouldLog = runtimeMode === 'cloud'
|
|
1297
|
-
? toolName === 'get_current_profile' || toolName === 'list_tasks' || durationMs >= 150
|
|
1432
|
+
? toolName === 'get_config' || toolName === 'get_current_profile' || toolName === 'list_tasks' || durationMs >= 150
|
|
1298
1433
|
: durationMs >= 250;
|
|
1299
1434
|
if (!shouldLog)
|
|
1300
1435
|
return;
|
|
@@ -1304,7 +1439,24 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1304
1439
|
...details,
|
|
1305
1440
|
});
|
|
1306
1441
|
}
|
|
1307
|
-
|
|
1442
|
+
// Tools eligible for bounded server-side retry on classified transient read failures.
|
|
1443
|
+
const retryableReadToolNames = new Set([
|
|
1444
|
+
// Safe reads called on every agent polling cycle
|
|
1445
|
+
'get_config',
|
|
1446
|
+
'get_current_profile',
|
|
1447
|
+
'list_tasks',
|
|
1448
|
+
// Bursty QA reads that can encounter transient DB contention
|
|
1449
|
+
'get_initiative',
|
|
1450
|
+
'get_workstream',
|
|
1451
|
+
'list_initiatives',
|
|
1452
|
+
'list_workstreams',
|
|
1453
|
+
'get_workstream_attachments',
|
|
1454
|
+
'get_initiative_attachments',
|
|
1455
|
+
'get_document',
|
|
1456
|
+
]);
|
|
1457
|
+
// Subset of retryable reads that use wrapper-level success logging.
|
|
1458
|
+
// Excluded: get_config, get_current_profile, list_tasks log from their own handlers.
|
|
1459
|
+
const wrapperObservedReadToolNames = new Set([
|
|
1308
1460
|
'get_initiative',
|
|
1309
1461
|
'get_workstream',
|
|
1310
1462
|
'list_initiatives',
|
|
@@ -1312,16 +1464,15 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1312
1464
|
'get_workstream_attachments',
|
|
1313
1465
|
'get_initiative_attachments',
|
|
1314
1466
|
'get_document',
|
|
1315
|
-
'read_document_content',
|
|
1316
1467
|
]);
|
|
1317
|
-
const
|
|
1468
|
+
const retryableReadRetryDelaysMs = [125, 350];
|
|
1318
1469
|
async function executeMcpToolWithReadRetry(toolName, args, handler) {
|
|
1319
1470
|
const startedAtMs = Date.now();
|
|
1320
1471
|
let attempt = 0;
|
|
1321
1472
|
for (;;) {
|
|
1322
1473
|
try {
|
|
1323
1474
|
const result = await handler(args);
|
|
1324
|
-
if (
|
|
1475
|
+
if (wrapperObservedReadToolNames.has(toolName)) {
|
|
1325
1476
|
logMcpReadObservation(toolName, startedAtMs, {
|
|
1326
1477
|
attempts: attempt + 1,
|
|
1327
1478
|
retried: attempt > 0,
|
|
@@ -1331,8 +1482,8 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1331
1482
|
}
|
|
1332
1483
|
catch (error) {
|
|
1333
1484
|
const transient = classifyTransientMcpError(error);
|
|
1334
|
-
if (!
|
|
1335
|
-
if (
|
|
1485
|
+
if (!retryableReadToolNames.has(toolName) || !transient || attempt >= retryableReadRetryDelaysMs.length) {
|
|
1486
|
+
if (retryableReadToolNames.has(toolName) && transient) {
|
|
1336
1487
|
logMcpStructuredEvent('warn', 'tool_read_retry_exhausted', {
|
|
1337
1488
|
toolName,
|
|
1338
1489
|
attempts: attempt + 1,
|
|
@@ -1343,7 +1494,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1343
1494
|
}
|
|
1344
1495
|
throw error;
|
|
1345
1496
|
}
|
|
1346
|
-
const delayMs =
|
|
1497
|
+
const delayMs = retryableReadRetryDelaysMs[attempt];
|
|
1347
1498
|
attempt += 1;
|
|
1348
1499
|
logMcpStructuredEvent('warn', 'tool_read_retry', {
|
|
1349
1500
|
toolName,
|
|
@@ -1598,17 +1749,24 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1598
1749
|
return 'unassigned';
|
|
1599
1750
|
return raw;
|
|
1600
1751
|
}
|
|
1752
|
+
function isCurrentAiAssigneeInput(value) {
|
|
1753
|
+
if (value === undefined || value === null)
|
|
1754
|
+
return false;
|
|
1755
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1756
|
+
return normalized === 'self';
|
|
1757
|
+
}
|
|
1601
1758
|
function formatEligibleAssigneeHint(actors) {
|
|
1602
1759
|
const eligible = actors.filter((actor) => String(actor.id || '').trim() && String(actor.id || '').trim().toLowerCase() !== 'agent');
|
|
1760
|
+
const selfHint = "Use 'self' to assign to the active AI profile, use 'unassigned' to clear ownership";
|
|
1603
1761
|
if (eligible.length === 0) {
|
|
1604
|
-
return
|
|
1762
|
+
return `${selfHint}, or pass a workspace member user ID or AI profile ID.`;
|
|
1605
1763
|
}
|
|
1606
1764
|
const preview = eligible
|
|
1607
1765
|
.slice(0, 8)
|
|
1608
1766
|
.map((actor) => `${actor.id} (${actor.label})`)
|
|
1609
1767
|
.join(', ');
|
|
1610
1768
|
const remainder = eligible.length > 8 ? `, plus ${eligible.length - 8} more` : '';
|
|
1611
|
-
return
|
|
1769
|
+
return `${selfHint}, or pass an eligible assignee ID from this workspace: ${preview}${remainder}.`;
|
|
1612
1770
|
}
|
|
1613
1771
|
function taskUriFor(id) {
|
|
1614
1772
|
return `${RESOURCE_URI_PREFIX}/tasks/${encodeURIComponent(id)}`;
|
|
@@ -1742,6 +1900,13 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1742
1900
|
name: options.serverName || (runtimeMode === 'cloud' ? 'taskforce-cloud' : PROJECT_NAME),
|
|
1743
1901
|
version: MCP_SERVER_VERSION,
|
|
1744
1902
|
runtimeMode,
|
|
1903
|
+
toolProfile: activeToolProfile,
|
|
1904
|
+
},
|
|
1905
|
+
mcpToolProfile: {
|
|
1906
|
+
active: activeToolProfile,
|
|
1907
|
+
available: ['default', 'extended', 'full'],
|
|
1908
|
+
defaultForRuntime: defaultToolProfile,
|
|
1909
|
+
guidance: 'Cloud sessions default to the default profile. Local/dev sessions default to full. Use TASKFORCE_MCP_TOOL_PROFILE=default|extended|full for local stdio, or pass an explicit toolProfile runtime option where supported.',
|
|
1745
1910
|
},
|
|
1746
1911
|
scope: {
|
|
1747
1912
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
@@ -2193,13 +2358,10 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2193
2358
|
const assigneeHint = describeEligibleAssignees();
|
|
2194
2359
|
const mutatingProfileTokenProperty = {
|
|
2195
2360
|
type: "string",
|
|
2196
|
-
description: "Optional
|
|
2361
|
+
description: "Optional profileToken from resolve_profile for stateless MCP clients."
|
|
2197
2362
|
};
|
|
2198
2363
|
const prefix = `[Project: ${PROJECT_NAME}] `;
|
|
2199
|
-
const
|
|
2200
|
-
? " NOTE: Use this tool instead of editing files directly to ensure data integrity."
|
|
2201
|
-
: "";
|
|
2202
|
-
const describeTool = (text) => prefix + text + agentNote;
|
|
2364
|
+
const describeTool = (text) => prefix + text;
|
|
2203
2365
|
const titleForTool = (name) => String(name || '')
|
|
2204
2366
|
.split('_')
|
|
2205
2367
|
.filter(Boolean)
|
|
@@ -2261,7 +2423,8 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2261
2423
|
}
|
|
2262
2424
|
}
|
|
2263
2425
|
};
|
|
2264
|
-
}).filter((tool) =>
|
|
2426
|
+
}).filter((tool) => (isMcpToolVisibleInProfile(tool.name, activeToolProfile)
|
|
2427
|
+
&& hasPermission(TOOL_PERMISSIONS[tool.name])));
|
|
2265
2428
|
return {
|
|
2266
2429
|
tools,
|
|
2267
2430
|
};
|
|
@@ -2314,58 +2477,82 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2314
2477
|
...(entity?.initiativeId ? { includeInitiatives: true, initiativeIds: [entity.initiativeId] } : {}),
|
|
2315
2478
|
});
|
|
2316
2479
|
};
|
|
2317
|
-
const
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
attachments
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2480
|
+
const buildTaskAttachmentContentPayload = async (task, args) => {
|
|
2481
|
+
const { attachmentId, format = "text", maxChars = 200000 } = args;
|
|
2482
|
+
const attachments = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
2483
|
+
const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
|
|
2484
|
+
if (!attachment) {
|
|
2485
|
+
throw new Error(`Attachment ${attachmentId} not found on task ${task.referenceLabel || task.id}`);
|
|
2486
|
+
}
|
|
2487
|
+
if (format === "metadata") {
|
|
2488
|
+
return {
|
|
2489
|
+
taskId: task.id,
|
|
2490
|
+
attachment,
|
|
2491
|
+
format: "metadata"
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
if (format === "download") {
|
|
2495
|
+
return {
|
|
2496
|
+
taskId: task.id,
|
|
2497
|
+
attachment,
|
|
2498
|
+
format: "download",
|
|
2499
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2500
|
+
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
if (!attachment.readableAsText) {
|
|
2504
|
+
return {
|
|
2505
|
+
taskId: task.id,
|
|
2506
|
+
attachment,
|
|
2507
|
+
format: "download",
|
|
2508
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2509
|
+
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
2513
|
+
const buffer = await canonicalAssetHelpers.readCanonicalAttachmentBuffer(task.id, attachment);
|
|
2514
|
+
const content = buffer.toString('utf8');
|
|
2515
|
+
if (content.length > resolvedMaxChars) {
|
|
2516
|
+
throw new Error('Attachment exceeds maxChars limit for inline read');
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
taskId: task.id,
|
|
2520
|
+
attachment,
|
|
2521
|
+
format: "text",
|
|
2522
|
+
truncated: false,
|
|
2523
|
+
content
|
|
2524
|
+
};
|
|
2525
|
+
};
|
|
2526
|
+
const buildPlanningAttachmentContentPayload = async (targetType, target, args) => {
|
|
2324
2527
|
const { attachmentId, format = "text", maxChars = 200000 } = args;
|
|
2325
|
-
const target = resolvePlanningAttachmentTarget(targetType, inputId);
|
|
2326
2528
|
const attachments = listPlanningAttachmentDescriptors(target);
|
|
2327
2529
|
const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
|
|
2328
2530
|
if (!attachment) {
|
|
2329
|
-
throw new Error(`Attachment ${attachmentId} not found on ${targetType} ${
|
|
2531
|
+
throw new Error(`Attachment ${attachmentId} not found on ${targetType} ${formatPlanningAttachmentTarget(target)}`);
|
|
2330
2532
|
}
|
|
2331
2533
|
if (format === "metadata") {
|
|
2332
2534
|
return {
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
[`${targetType}Id`]: target.id,
|
|
2337
|
-
attachment,
|
|
2338
|
-
format: "metadata"
|
|
2339
|
-
}, null, 2)
|
|
2340
|
-
}],
|
|
2535
|
+
[`${targetType}Id`]: target.id,
|
|
2536
|
+
attachment,
|
|
2537
|
+
format: "metadata"
|
|
2341
2538
|
};
|
|
2342
2539
|
}
|
|
2343
2540
|
if (format === "download") {
|
|
2344
2541
|
return {
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
format: "download",
|
|
2351
|
-
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2352
|
-
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
2353
|
-
}, null, 2)
|
|
2354
|
-
}],
|
|
2542
|
+
[`${targetType}Id`]: target.id,
|
|
2543
|
+
attachment,
|
|
2544
|
+
format: "download",
|
|
2545
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2546
|
+
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
2355
2547
|
};
|
|
2356
2548
|
}
|
|
2357
2549
|
if (!attachment.readableAsText) {
|
|
2358
2550
|
return {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
format: "download",
|
|
2365
|
-
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2366
|
-
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
2367
|
-
}, null, 2)
|
|
2368
|
-
}],
|
|
2551
|
+
[`${targetType}Id`]: target.id,
|
|
2552
|
+
attachment,
|
|
2553
|
+
format: "download",
|
|
2554
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
2555
|
+
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
2369
2556
|
};
|
|
2370
2557
|
}
|
|
2371
2558
|
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
@@ -2375,16 +2562,24 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2375
2562
|
throw new Error('Attachment exceeds maxChars limit for inline read');
|
|
2376
2563
|
}
|
|
2377
2564
|
return {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2565
|
+
[`${targetType}Id`]: target.id,
|
|
2566
|
+
attachment,
|
|
2567
|
+
format: "text",
|
|
2568
|
+
truncated: false,
|
|
2569
|
+
content
|
|
2570
|
+
};
|
|
2571
|
+
};
|
|
2572
|
+
const buildPlanningAttachmentListPayload = async (targetType, target, args = {}) => {
|
|
2573
|
+
const attachmentId = typeof args.attachmentId === 'string' && args.attachmentId.trim().length > 0
|
|
2574
|
+
? args.attachmentId
|
|
2575
|
+
: null;
|
|
2576
|
+
return {
|
|
2577
|
+
[`${targetType}Id`]: target.id,
|
|
2578
|
+
referenceLabel: target.referenceLabel,
|
|
2579
|
+
attachments: listPlanningAttachmentDescriptors(target),
|
|
2580
|
+
...(attachmentId
|
|
2581
|
+
? { attachmentContent: await buildPlanningAttachmentContentPayload(targetType, target, { ...args, attachmentId }) }
|
|
2582
|
+
: {}),
|
|
2388
2583
|
};
|
|
2389
2584
|
};
|
|
2390
2585
|
const savePlanningAttachment = async (targetType, args) => {
|
|
@@ -2582,11 +2777,12 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2582
2777
|
case "list_tasks": {
|
|
2583
2778
|
const startedAtMs = Date.now();
|
|
2584
2779
|
const { format = "markdown", category, status, openOnly = false, assignee, priority, type, sort = "created", order = "desc", limit } = args;
|
|
2780
|
+
const resolvedAssignee = resolveListTasksAssigneeInput(assignee);
|
|
2585
2781
|
const slimTasks = core.listTasksSlim(ACTIVE_WORKSPACE_ID, {
|
|
2586
2782
|
category,
|
|
2587
2783
|
status,
|
|
2588
2784
|
openOnly: Boolean(openOnly),
|
|
2589
|
-
assignee,
|
|
2785
|
+
assignee: resolvedAssignee,
|
|
2590
2786
|
priority: priority !== undefined ? Number(priority) : undefined,
|
|
2591
2787
|
type,
|
|
2592
2788
|
sort,
|
|
@@ -2597,7 +2793,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2597
2793
|
format,
|
|
2598
2794
|
returnedTasks: slimTasks.length,
|
|
2599
2795
|
openOnly,
|
|
2600
|
-
hasAssigneeFilter: Boolean(
|
|
2796
|
+
hasAssigneeFilter: Boolean(resolvedAssignee),
|
|
2601
2797
|
hasStatusFilter: Boolean(status),
|
|
2602
2798
|
hasCategoryFilter: Boolean(category),
|
|
2603
2799
|
hasTypeFilter: Boolean(type),
|
|
@@ -2645,28 +2841,13 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2645
2841
|
profileId: resolved.profile.id,
|
|
2646
2842
|
created: resolved.created
|
|
2647
2843
|
});
|
|
2844
|
+
const attribution = buildAiProfileAttribution(resolved.profile, 'session');
|
|
2648
2845
|
return {
|
|
2649
2846
|
content: [{
|
|
2650
2847
|
type: "text",
|
|
2651
2848
|
text: JSON.stringify({
|
|
2652
2849
|
created: resolved.created,
|
|
2653
|
-
|
|
2654
|
-
profile: {
|
|
2655
|
-
id: resolved.profile.id,
|
|
2656
|
-
workspaceId: resolved.profile.workspaceId,
|
|
2657
|
-
profileToken: resolved.profile.profileToken,
|
|
2658
|
-
name: resolved.profile.name,
|
|
2659
|
-
username: resolved.profile.username,
|
|
2660
|
-
icon: resolved.profile.icon,
|
|
2661
|
-
color: resolved.profile.color,
|
|
2662
|
-
description: resolved.profile.description ?? null,
|
|
2663
|
-
role: resolved.profile.role ?? null,
|
|
2664
|
-
provider: resolved.profile.provider ?? null,
|
|
2665
|
-
model: resolved.profile.model ?? null,
|
|
2666
|
-
surfaceType: resolved.profile.surfaceType ?? null,
|
|
2667
|
-
seatScope: resolved.profile.seatScope ?? null,
|
|
2668
|
-
lastActiveAt: resolved.profile.lastActiveAt
|
|
2669
|
-
},
|
|
2850
|
+
...attribution,
|
|
2670
2851
|
...(resolved.duplicateWarning && { duplicateWarning: resolved.duplicateWarning })
|
|
2671
2852
|
}, null, 2)
|
|
2672
2853
|
}],
|
|
@@ -2702,10 +2883,11 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2702
2883
|
}
|
|
2703
2884
|
bindingSource = 'token_lookup';
|
|
2704
2885
|
}
|
|
2886
|
+
const attribution = buildAiProfileAttribution(profile, bindingSource);
|
|
2705
2887
|
logMcpReadObservation('get_current_profile', startedAtMs, {
|
|
2706
|
-
identityBound:
|
|
2707
|
-
bindingSource,
|
|
2708
|
-
writeReady:
|
|
2888
|
+
identityBound: attribution.identityBound,
|
|
2889
|
+
bindingSource: attribution.bindingSource,
|
|
2890
|
+
writeReady: attribution.writeReady,
|
|
2709
2891
|
profileResolved: Boolean(profile),
|
|
2710
2892
|
usedProfileTokenLookup: !activeAiProfileId && typeof profileToken === 'string' && profileToken.trim().length > 0,
|
|
2711
2893
|
});
|
|
@@ -2714,19 +2896,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2714
2896
|
type: "text",
|
|
2715
2897
|
text: JSON.stringify({
|
|
2716
2898
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
2717
|
-
|
|
2718
|
-
bindingSource,
|
|
2719
|
-
writeReady: Boolean(profile),
|
|
2720
|
-
profileToken: profile?.profileToken ?? null,
|
|
2721
|
-
profile: profile ? buildAiProfileDescriptor(profile) : null,
|
|
2722
|
-
taskFilter: profile ? { assignee: profile.id } : null,
|
|
2723
|
-
nextTool: profile ? 'list_tasks' : 'resolve_profile',
|
|
2724
|
-
suggestedArgs: profile
|
|
2725
|
-
? { assignee: profile.id, format: 'json' }
|
|
2726
|
-
: { name: 'Your AI name' },
|
|
2727
|
-
guidance: profile
|
|
2728
|
-
? "Use suggestedArgs with list_tasks to get tasks assigned to this AI profile."
|
|
2729
|
-
: "No AI profile is currently bound. Call resolve_profile first, then use the returned profile id as the assignee filter for list_tasks.",
|
|
2899
|
+
...attribution,
|
|
2730
2900
|
}, null, 2)
|
|
2731
2901
|
}],
|
|
2732
2902
|
};
|
|
@@ -2933,92 +3103,78 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2933
3103
|
content: [{ type: "text", text: JSON.stringify(buildPostWriteWorkstreamResourcePayload(unarchived, 'unarchive_workstream:response'), null, 2) }],
|
|
2934
3104
|
};
|
|
2935
3105
|
}
|
|
2936
|
-
case "
|
|
2937
|
-
const { taskId, workstreamId } = args;
|
|
2938
|
-
const
|
|
2939
|
-
const { id:
|
|
2940
|
-
|
|
2941
|
-
workstreamId
|
|
2942
|
-
}, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
|
|
2943
|
-
if (!updated) {
|
|
2944
|
-
throw new Error(buildTaskLookupFailureMessage(String(taskId || '').trim()));
|
|
3106
|
+
case "update_task_workstream": {
|
|
3107
|
+
const { id: legacyInputId, taskId, workstreamId } = args;
|
|
3108
|
+
const inputId = taskId ?? legacyInputId;
|
|
3109
|
+
const { id: resolvedTaskId, task } = resolveTaskByIdentifierOrThrow(inputId);
|
|
3110
|
+
if (workstreamId === undefined) {
|
|
3111
|
+
throw new Error('workstreamId is required; pass null or an empty string to unlink the task.');
|
|
2945
3112
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
workstreamIds: [resolvedWorkstreamId],
|
|
2952
|
-
});
|
|
2953
|
-
return {
|
|
2954
|
-
content: [{
|
|
2955
|
-
type: "text",
|
|
2956
|
-
text: JSON.stringify({
|
|
2957
|
-
task: buildTaskWriteSummary(updated),
|
|
2958
|
-
workstreamId: resolvedWorkstreamId,
|
|
2959
|
-
}, null, 2)
|
|
2960
|
-
}],
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
|
-
case "clear_task_workstream": {
|
|
2964
|
-
const { taskId } = args;
|
|
2965
|
-
const { id: resolvedTaskId, task } = resolveTaskByIdentifierOrThrow(taskId);
|
|
2966
|
-
const priorWorkstreamId = task.workstreamId || null;
|
|
3113
|
+
const normalizedWorkstreamId = typeof workstreamId === 'string' ? workstreamId.trim() : workstreamId;
|
|
3114
|
+
const nextWorkstreamId = normalizedWorkstreamId === null || normalizedWorkstreamId === ''
|
|
3115
|
+
? null
|
|
3116
|
+
: resolveWorkstreamByIdentifierOrThrow(normalizedWorkstreamId).id;
|
|
3117
|
+
const previousWorkstreamId = task.workstreamId || null;
|
|
2967
3118
|
const updated = core.updateTask(resolvedTaskId, {
|
|
2968
|
-
workstreamId:
|
|
3119
|
+
workstreamId: nextWorkstreamId,
|
|
2969
3120
|
}, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
|
|
2970
3121
|
if (!updated) {
|
|
2971
|
-
throw new Error(buildTaskLookupFailureMessage(String(
|
|
3122
|
+
throw new Error(buildTaskLookupFailureMessage(String(inputId || '').trim()));
|
|
2972
3123
|
}
|
|
2973
|
-
|
|
3124
|
+
const relatedWorkstreamIds = new Set();
|
|
3125
|
+
if (previousWorkstreamId)
|
|
3126
|
+
relatedWorkstreamIds.add(previousWorkstreamId);
|
|
3127
|
+
if (nextWorkstreamId)
|
|
3128
|
+
relatedWorkstreamIds.add(nextWorkstreamId);
|
|
3129
|
+
recordMcpWriteAudit('update_task_workstream', {
|
|
3130
|
+
taskId: updated.id,
|
|
3131
|
+
workstreamId: nextWorkstreamId,
|
|
3132
|
+
previousWorkstreamId,
|
|
3133
|
+
});
|
|
2974
3134
|
await notifyResourceMutation({
|
|
2975
3135
|
includeTasks: true,
|
|
2976
3136
|
taskIds: [updated.id],
|
|
2977
|
-
...(
|
|
3137
|
+
...(relatedWorkstreamIds.size > 0 ? { includeWorkstreams: true, workstreamIds: Array.from(relatedWorkstreamIds) } : {}),
|
|
2978
3138
|
});
|
|
2979
3139
|
return {
|
|
2980
3140
|
content: [{
|
|
2981
3141
|
type: "text",
|
|
2982
3142
|
text: JSON.stringify({
|
|
2983
3143
|
task: buildTaskWriteSummary(updated),
|
|
2984
|
-
workstreamId:
|
|
2985
|
-
previousWorkstreamId
|
|
3144
|
+
workstreamId: nextWorkstreamId,
|
|
3145
|
+
previousWorkstreamId,
|
|
2986
3146
|
}, null, 2)
|
|
2987
3147
|
}],
|
|
2988
3148
|
};
|
|
2989
3149
|
}
|
|
2990
|
-
case "
|
|
2991
|
-
const { workstreamId, initiativeId } = args;
|
|
2992
|
-
const
|
|
2993
|
-
const { id:
|
|
3150
|
+
case "update_workstream_initiative": {
|
|
3151
|
+
const { id: legacyInputId, workstreamId, initiativeId } = args;
|
|
3152
|
+
const inputId = workstreamId ?? legacyInputId;
|
|
3153
|
+
const { id: resolvedWorkstreamId, workstream: existingWorkstream } = resolveWorkstreamByIdentifierOrThrow(inputId);
|
|
3154
|
+
if (initiativeId === undefined) {
|
|
3155
|
+
throw new Error('initiativeId is required; pass null or an empty string to unlink the workstream.');
|
|
3156
|
+
}
|
|
3157
|
+
const normalizedInitiativeId = typeof initiativeId === 'string' ? initiativeId.trim() : initiativeId;
|
|
3158
|
+
const nextInitiativeId = normalizedInitiativeId === null || normalizedInitiativeId === ''
|
|
3159
|
+
? null
|
|
3160
|
+
: resolveInitiativeByIdentifierOrThrow(normalizedInitiativeId).id;
|
|
2994
3161
|
const updated = core.updateWorkstream(resolvedWorkstreamId, {
|
|
2995
|
-
initiativeId:
|
|
3162
|
+
initiativeId: nextInitiativeId,
|
|
2996
3163
|
}, ACTIVE_WORKSPACE_ID);
|
|
2997
|
-
const relatedInitiativeIds = new Set(
|
|
3164
|
+
const relatedInitiativeIds = new Set();
|
|
2998
3165
|
if (existingWorkstream.initiativeId)
|
|
2999
3166
|
relatedInitiativeIds.add(existingWorkstream.initiativeId);
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3167
|
+
if (nextInitiativeId)
|
|
3168
|
+
relatedInitiativeIds.add(nextInitiativeId);
|
|
3169
|
+
recordMcpWriteAudit('update_workstream_initiative', {
|
|
3170
|
+
workstreamId: updated.id,
|
|
3171
|
+
initiativeId: nextInitiativeId,
|
|
3172
|
+
previousInitiativeId: existingWorkstream.initiativeId || null,
|
|
3006
3173
|
});
|
|
3007
|
-
return {
|
|
3008
|
-
content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(updated), null, 2) }],
|
|
3009
|
-
};
|
|
3010
|
-
}
|
|
3011
|
-
case "clear_workstream_initiative": {
|
|
3012
|
-
const { workstreamId } = args;
|
|
3013
|
-
const { id: resolvedWorkstreamId, workstream: existingWorkstream } = resolveWorkstreamByIdentifierOrThrow(workstreamId);
|
|
3014
|
-
const updated = core.updateWorkstream(resolvedWorkstreamId, {
|
|
3015
|
-
initiativeId: null,
|
|
3016
|
-
}, ACTIVE_WORKSPACE_ID);
|
|
3017
|
-
recordMcpWriteAudit('clear_workstream_initiative', { workstreamId: updated.id, previousInitiativeId: existingWorkstream.initiativeId || null });
|
|
3018
3174
|
await notifyResourceMutation({
|
|
3019
3175
|
includeWorkstreams: true,
|
|
3020
3176
|
workstreamIds: [updated.id],
|
|
3021
|
-
...(
|
|
3177
|
+
...(relatedInitiativeIds.size > 0 ? { includeInitiatives: true, initiativeIds: Array.from(relatedInitiativeIds) } : {}),
|
|
3022
3178
|
});
|
|
3023
3179
|
return {
|
|
3024
3180
|
content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(updated), null, 2) }],
|
|
@@ -3079,7 +3235,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3079
3235
|
};
|
|
3080
3236
|
}
|
|
3081
3237
|
case "get_task_attachments": {
|
|
3082
|
-
const { id, taskId } = args;
|
|
3238
|
+
const { id, taskId, attachmentId } = args;
|
|
3083
3239
|
const taskIdentifier = taskId ?? id;
|
|
3084
3240
|
const task = core.resolveTaskIdentifier(taskIdentifier, ACTIVE_WORKSPACE_ID);
|
|
3085
3241
|
if (!task) {
|
|
@@ -3091,77 +3247,10 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3091
3247
|
text: JSON.stringify({
|
|
3092
3248
|
taskId: task.id,
|
|
3093
3249
|
referenceLabel: task.referenceLabel || getTaskReferenceLabel(task),
|
|
3094
|
-
attachments: taskAttachmentHelpers.listTaskAttachmentDescriptors(task)
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
}
|
|
3099
|
-
case "read_task_attachment": {
|
|
3100
|
-
const { taskId, attachmentId, format = "text", maxChars = 200000 } = args;
|
|
3101
|
-
const task = core.resolveTaskIdentifier(taskId, ACTIVE_WORKSPACE_ID);
|
|
3102
|
-
if (!task) {
|
|
3103
|
-
throw new Error(`Task ${taskId} not found`);
|
|
3104
|
-
}
|
|
3105
|
-
const attachments = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
3106
|
-
const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
|
|
3107
|
-
if (!attachment) {
|
|
3108
|
-
throw new Error(`Attachment ${attachmentId} not found on task ${taskId}`);
|
|
3109
|
-
}
|
|
3110
|
-
if (format === "metadata") {
|
|
3111
|
-
return {
|
|
3112
|
-
content: [{
|
|
3113
|
-
type: "text",
|
|
3114
|
-
text: JSON.stringify({
|
|
3115
|
-
taskId: task.id,
|
|
3116
|
-
attachment,
|
|
3117
|
-
format: "metadata"
|
|
3118
|
-
}, null, 2)
|
|
3119
|
-
}],
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
if (format === "download") {
|
|
3123
|
-
return {
|
|
3124
|
-
content: [{
|
|
3125
|
-
type: "text",
|
|
3126
|
-
text: JSON.stringify({
|
|
3127
|
-
taskId: task.id,
|
|
3128
|
-
attachment,
|
|
3129
|
-
format: "download",
|
|
3130
|
-
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3131
|
-
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
3132
|
-
}, null, 2)
|
|
3133
|
-
}],
|
|
3134
|
-
};
|
|
3135
|
-
}
|
|
3136
|
-
if (!attachment.readableAsText) {
|
|
3137
|
-
return {
|
|
3138
|
-
content: [{
|
|
3139
|
-
type: "text",
|
|
3140
|
-
text: JSON.stringify({
|
|
3141
|
-
taskId: task.id,
|
|
3142
|
-
attachment,
|
|
3143
|
-
format: "download",
|
|
3144
|
-
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3145
|
-
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
3146
|
-
}, null, 2)
|
|
3147
|
-
}],
|
|
3148
|
-
};
|
|
3149
|
-
}
|
|
3150
|
-
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
3151
|
-
const buffer = await canonicalAssetHelpers.readCanonicalAttachmentBuffer(task.id, attachment);
|
|
3152
|
-
const content = buffer.toString('utf8');
|
|
3153
|
-
if (content.length > resolvedMaxChars) {
|
|
3154
|
-
throw new Error('Attachment exceeds maxChars limit for inline read');
|
|
3155
|
-
}
|
|
3156
|
-
return {
|
|
3157
|
-
content: [{
|
|
3158
|
-
type: "text",
|
|
3159
|
-
text: JSON.stringify({
|
|
3160
|
-
taskId: task.id,
|
|
3161
|
-
attachment,
|
|
3162
|
-
format: "text",
|
|
3163
|
-
truncated: false,
|
|
3164
|
-
content
|
|
3250
|
+
attachments: taskAttachmentHelpers.listTaskAttachmentDescriptors(task),
|
|
3251
|
+
...(typeof attachmentId === 'string' && attachmentId.trim().length > 0
|
|
3252
|
+
? { attachmentContent: await buildTaskAttachmentContentPayload(task, args) }
|
|
3253
|
+
: {}),
|
|
3165
3254
|
}, null, 2)
|
|
3166
3255
|
}],
|
|
3167
3256
|
};
|
|
@@ -3172,26 +3261,20 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3172
3261
|
return {
|
|
3173
3262
|
content: [{
|
|
3174
3263
|
type: "text",
|
|
3175
|
-
text: JSON.stringify(buildPlanningAttachmentListPayload('workstream', target), null, 2)
|
|
3264
|
+
text: JSON.stringify(await buildPlanningAttachmentListPayload('workstream', target, args), null, 2)
|
|
3176
3265
|
}],
|
|
3177
3266
|
};
|
|
3178
3267
|
}
|
|
3179
|
-
case "read_workstream_attachment": {
|
|
3180
|
-
return readPlanningAttachment('workstream', args);
|
|
3181
|
-
}
|
|
3182
3268
|
case "get_initiative_attachments": {
|
|
3183
3269
|
const { id } = args;
|
|
3184
3270
|
const target = resolvePlanningAttachmentTarget('initiative', id);
|
|
3185
3271
|
return {
|
|
3186
3272
|
content: [{
|
|
3187
3273
|
type: "text",
|
|
3188
|
-
text: JSON.stringify(buildPlanningAttachmentListPayload('initiative', target), null, 2)
|
|
3274
|
+
text: JSON.stringify(await buildPlanningAttachmentListPayload('initiative', target, args), null, 2)
|
|
3189
3275
|
}],
|
|
3190
3276
|
};
|
|
3191
3277
|
}
|
|
3192
|
-
case "read_initiative_attachment": {
|
|
3193
|
-
return readPlanningAttachment('initiative', args);
|
|
3194
|
-
}
|
|
3195
3278
|
case "search_images": {
|
|
3196
3279
|
assertAnnotatedAttachmentReadAccess();
|
|
3197
3280
|
const { query, limit } = args;
|
|
@@ -3298,14 +3381,14 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3298
3381
|
query: typeof query === 'string' ? query.trim() : '',
|
|
3299
3382
|
results,
|
|
3300
3383
|
guidance: results.length > 0
|
|
3301
|
-
? 'After picking a match, call get_document with the suggestedArgs.documentId
|
|
3384
|
+
? 'After picking a match, call get_document with the suggestedArgs.documentId. Pass includeContent=true when you need the body.'
|
|
3302
3385
|
: 'No matching canonical documents were found. Try a D-123 reference, document id, title keyword, or filename fragment.',
|
|
3303
3386
|
}, null, 2)
|
|
3304
3387
|
}],
|
|
3305
3388
|
};
|
|
3306
3389
|
}
|
|
3307
3390
|
case "get_document": {
|
|
3308
|
-
const { id, documentId } = args;
|
|
3391
|
+
const { id, documentId, includeContent, format, maxChars } = args;
|
|
3309
3392
|
const asset = resolveDocumentAssetByIdentifierOrThrow(documentId ?? id);
|
|
3310
3393
|
const linkedTasks = (workspaceAssetStore?.listLinksForAsset(asset.assetId, ACTIVE_WORKSPACE_ID) || [])
|
|
3311
3394
|
.filter((entry) => !entry.deletedAt)
|
|
@@ -3331,6 +3414,11 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3331
3414
|
updatedAt: session.updatedAt,
|
|
3332
3415
|
commentCount: Array.isArray(session.comments) ? session.comments.length : 0,
|
|
3333
3416
|
}));
|
|
3417
|
+
const normalizedFormat = typeof format === 'string' ? format.trim().toLowerCase() : '';
|
|
3418
|
+
const shouldIncludeContent = includeContent === true || ['content', 'full', 'text', 'download'].includes(normalizedFormat);
|
|
3419
|
+
const documentContent = shouldIncludeContent
|
|
3420
|
+
? await buildDocumentContentPayload(asset, { format, maxChars })
|
|
3421
|
+
: null;
|
|
3334
3422
|
return {
|
|
3335
3423
|
content: [{
|
|
3336
3424
|
type: "text",
|
|
@@ -3339,83 +3427,46 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3339
3427
|
readContent: buildDocumentReadHint(asset),
|
|
3340
3428
|
linkedTasks,
|
|
3341
3429
|
reviewSessions,
|
|
3430
|
+
...(documentContent ? { documentContent } : {}),
|
|
3342
3431
|
}, null, 2)
|
|
3343
3432
|
}],
|
|
3344
3433
|
};
|
|
3345
3434
|
}
|
|
3346
|
-
case "
|
|
3347
|
-
|
|
3348
|
-
const
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
document,
|
|
3356
|
-
format: "metadata"
|
|
3357
|
-
}, null, 2)
|
|
3358
|
-
}],
|
|
3359
|
-
};
|
|
3435
|
+
case "get_image_annotations": {
|
|
3436
|
+
assertAnnotatedAttachmentReadAccess();
|
|
3437
|
+
const { taskId, imageId, imageAssetId, sessionId } = args;
|
|
3438
|
+
const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
3439
|
+
const normalizedImageIdentifier = typeof imageId === 'string' && imageId.trim()
|
|
3440
|
+
? imageId.trim()
|
|
3441
|
+
: (typeof imageAssetId === 'string' ? imageAssetId.trim() || null : null);
|
|
3442
|
+
if (!taskId && !normalizedImageIdentifier && !normalizedSessionId) {
|
|
3443
|
+
throw new Error('taskId, imageId, or sessionId is required');
|
|
3360
3444
|
}
|
|
3361
|
-
|
|
3445
|
+
const payload = normalizedSessionId
|
|
3446
|
+
? core.buildAnnotatedAttachmentPayload({
|
|
3447
|
+
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3448
|
+
sessionId: normalizedSessionId,
|
|
3449
|
+
})
|
|
3450
|
+
: null;
|
|
3451
|
+
if (!taskId && !normalizedImageIdentifier) {
|
|
3362
3452
|
return {
|
|
3363
3453
|
content: [{
|
|
3364
3454
|
type: "text",
|
|
3365
|
-
text: JSON.stringify({
|
|
3366
|
-
document,
|
|
3367
|
-
format: "download",
|
|
3368
|
-
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
3369
|
-
reason: "requested_download"
|
|
3370
|
-
}, null, 2)
|
|
3455
|
+
text: JSON.stringify({ payload }, null, 2)
|
|
3371
3456
|
}],
|
|
3372
3457
|
};
|
|
3373
3458
|
}
|
|
3374
|
-
if (!isTextReadableMimeType(asset.mimeType || '')) {
|
|
3375
|
-
return {
|
|
3376
|
-
content: [{
|
|
3377
|
-
type: "text",
|
|
3378
|
-
text: JSON.stringify({
|
|
3379
|
-
document,
|
|
3380
|
-
format: "download",
|
|
3381
|
-
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
3382
|
-
reason: "binary_file"
|
|
3383
|
-
}, null, 2)
|
|
3384
|
-
}],
|
|
3385
|
-
};
|
|
3386
|
-
}
|
|
3387
|
-
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
3388
|
-
const buffer = await canonicalAssetHelpers.readCanonicalDocumentBuffer(asset);
|
|
3389
|
-
const content = buffer.toString('utf8');
|
|
3390
|
-
if (content.length > resolvedMaxChars) {
|
|
3391
|
-
throw new Error('Document exceeds maxChars limit for inline read');
|
|
3392
|
-
}
|
|
3393
|
-
return {
|
|
3394
|
-
content: [{
|
|
3395
|
-
type: "text",
|
|
3396
|
-
text: JSON.stringify({
|
|
3397
|
-
document,
|
|
3398
|
-
format: "text",
|
|
3399
|
-
truncated: false,
|
|
3400
|
-
content
|
|
3401
|
-
}, null, 2)
|
|
3402
|
-
}],
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
case "get_annotated_attachment_sessions": {
|
|
3406
|
-
assertAnnotatedAttachmentReadAccess();
|
|
3407
|
-
const { taskId, imageId, imageAssetId } = args;
|
|
3408
|
-
const normalizedImageIdentifier = typeof imageId === 'string' && imageId.trim()
|
|
3409
|
-
? imageId.trim()
|
|
3410
|
-
: (typeof imageAssetId === 'string' ? imageAssetId.trim() || null : null);
|
|
3411
|
-
if (!taskId && !normalizedImageIdentifier) {
|
|
3412
|
-
throw new Error('taskId or imageId is required');
|
|
3413
|
-
}
|
|
3414
3459
|
const image = normalizedImageIdentifier
|
|
3415
3460
|
? resolveImageAssetByIdentifierOrThrow(normalizedImageIdentifier)
|
|
3416
3461
|
: null;
|
|
3417
3462
|
if (taskId) {
|
|
3418
3463
|
const { task } = resolveTaskByIdentifierOrThrow(taskId);
|
|
3464
|
+
if (payload && payload.taskId !== task.id) {
|
|
3465
|
+
throw new Error(`Annotated attachment session ${normalizedSessionId} is not linked to task ${task.referenceLabel || task.id}.`);
|
|
3466
|
+
}
|
|
3467
|
+
if (payload && image && payload.image.assetId !== image.assetId) {
|
|
3468
|
+
throw new Error(`Annotated attachment session ${normalizedSessionId} is not linked to image ${image.assetId}.`);
|
|
3469
|
+
}
|
|
3419
3470
|
const sessions = core.listAnnotatedAttachmentSessions({
|
|
3420
3471
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3421
3472
|
taskId: task.id,
|
|
@@ -3428,11 +3479,15 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3428
3479
|
taskId: task.id,
|
|
3429
3480
|
referenceLabel: task.referenceLabel || getTaskReferenceLabel(task),
|
|
3430
3481
|
image: image ? buildImageDescriptor(image) : null,
|
|
3431
|
-
sessions
|
|
3482
|
+
sessions,
|
|
3483
|
+
...(payload ? { payload } : {}),
|
|
3432
3484
|
}, null, 2)
|
|
3433
3485
|
}],
|
|
3434
3486
|
};
|
|
3435
3487
|
}
|
|
3488
|
+
if (payload && image && payload.image.assetId !== image.assetId) {
|
|
3489
|
+
throw new Error(`Annotated attachment session ${normalizedSessionId} is not linked to image ${image.assetId}.`);
|
|
3490
|
+
}
|
|
3436
3491
|
const sessionsForImage = core.listAnnotatedAttachmentSessions({
|
|
3437
3492
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3438
3493
|
imageAssetId: image.assetId,
|
|
@@ -3462,27 +3517,11 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3462
3517
|
image: buildImageDescriptor(image),
|
|
3463
3518
|
tasks: sessionsByTask,
|
|
3464
3519
|
unattachedSessions,
|
|
3520
|
+
...(payload ? { payload } : {}),
|
|
3465
3521
|
}, null, 2)
|
|
3466
3522
|
}],
|
|
3467
3523
|
};
|
|
3468
3524
|
}
|
|
3469
|
-
case "read_annotated_attachment_payload": {
|
|
3470
|
-
assertAnnotatedAttachmentReadAccess();
|
|
3471
|
-
const { sessionId } = args;
|
|
3472
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
3473
|
-
throw new Error('sessionId is required');
|
|
3474
|
-
}
|
|
3475
|
-
const payload = core.buildAnnotatedAttachmentPayload({
|
|
3476
|
-
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3477
|
-
sessionId: sessionId.trim(),
|
|
3478
|
-
});
|
|
3479
|
-
return {
|
|
3480
|
-
content: [{
|
|
3481
|
-
type: "text",
|
|
3482
|
-
text: JSON.stringify(payload, null, 2)
|
|
3483
|
-
}],
|
|
3484
|
-
};
|
|
3485
|
-
}
|
|
3486
3525
|
case "create_annotated_attachment_session": {
|
|
3487
3526
|
assertAnnotatedAttachmentReadAccess();
|
|
3488
3527
|
const { imageId, taskId, title, sessionInstruction } = args;
|
|
@@ -3738,9 +3777,37 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3738
3777
|
}],
|
|
3739
3778
|
};
|
|
3740
3779
|
}
|
|
3741
|
-
case "
|
|
3742
|
-
const { documentId } = args;
|
|
3743
|
-
const
|
|
3780
|
+
case "get_document_reviews": {
|
|
3781
|
+
const { documentId, id: legacyInputId, sessionId } = args;
|
|
3782
|
+
const normalizedSessionId = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
3783
|
+
if (normalizedSessionId) {
|
|
3784
|
+
const inputId = documentId ?? legacyInputId;
|
|
3785
|
+
const requestedAsset = typeof inputId === 'string' && inputId.trim()
|
|
3786
|
+
? resolveDocumentAssetByIdentifierOrThrow(inputId)
|
|
3787
|
+
: null;
|
|
3788
|
+
const session = core.getDocumentReviewSession({
|
|
3789
|
+
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3790
|
+
sessionId: normalizedSessionId,
|
|
3791
|
+
});
|
|
3792
|
+
if (!session) {
|
|
3793
|
+
throw new Error(`Document review session ${sessionId} not found.`);
|
|
3794
|
+
}
|
|
3795
|
+
if (requestedAsset && session.assetId !== requestedAsset.assetId) {
|
|
3796
|
+
throw new Error(`Document review session ${normalizedSessionId} is not linked to document ${requestedAsset.assetId}.`);
|
|
3797
|
+
}
|
|
3798
|
+
const asset = workspaceAssetStore?.get(session.assetId, ACTIVE_WORKSPACE_ID) || null;
|
|
3799
|
+
return {
|
|
3800
|
+
content: [{
|
|
3801
|
+
type: "text",
|
|
3802
|
+
text: JSON.stringify({
|
|
3803
|
+
document: asset ? buildDocumentDescriptor(asset) : null,
|
|
3804
|
+
session
|
|
3805
|
+
}, null, 2)
|
|
3806
|
+
}],
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
const inputId = documentId ?? legacyInputId;
|
|
3810
|
+
const asset = resolveDocumentAssetByIdentifierOrThrow(inputId);
|
|
3744
3811
|
const sessions = core.listDocumentReviewSessions({
|
|
3745
3812
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3746
3813
|
assetId: asset.assetId,
|
|
@@ -3755,29 +3822,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3755
3822
|
}],
|
|
3756
3823
|
};
|
|
3757
3824
|
}
|
|
3758
|
-
case "get_document_review_session": {
|
|
3759
|
-
const { sessionId } = args;
|
|
3760
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
3761
|
-
throw new Error('sessionId is required');
|
|
3762
|
-
}
|
|
3763
|
-
const session = core.getDocumentReviewSession({
|
|
3764
|
-
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
3765
|
-
sessionId: sessionId.trim(),
|
|
3766
|
-
});
|
|
3767
|
-
if (!session) {
|
|
3768
|
-
throw new Error(`Document review session ${sessionId} not found.`);
|
|
3769
|
-
}
|
|
3770
|
-
const asset = workspaceAssetStore?.get(session.assetId, ACTIVE_WORKSPACE_ID) || null;
|
|
3771
|
-
return {
|
|
3772
|
-
content: [{
|
|
3773
|
-
type: "text",
|
|
3774
|
-
text: JSON.stringify({
|
|
3775
|
-
document: asset ? buildDocumentDescriptor(asset) : null,
|
|
3776
|
-
session
|
|
3777
|
-
}, null, 2)
|
|
3778
|
-
}],
|
|
3779
|
-
};
|
|
3780
|
-
}
|
|
3781
3825
|
case "create_document_review_session": {
|
|
3782
3826
|
const { documentId, title, status } = args;
|
|
3783
3827
|
const asset = resolveDocumentAssetByIdentifierOrThrow(documentId);
|
|
@@ -4019,51 +4063,60 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4019
4063
|
content: [{ type: "text", text: JSON.stringify(board, null, 2) }],
|
|
4020
4064
|
};
|
|
4021
4065
|
}
|
|
4022
|
-
case "
|
|
4023
|
-
const { id: legacyInputId, taskId, scheduledDate: legacyScheduledDate, date, orderInDay: legacyOrderInDay, order } = args;
|
|
4066
|
+
case "update_schedule": {
|
|
4067
|
+
const { id: legacyInputId, taskId, scheduledDate: legacyScheduledDate, date, orderInDay: legacyOrderInDay, order, moves, taskIds, } = args;
|
|
4068
|
+
const inputMoves = Array.isArray(moves)
|
|
4069
|
+
? moves
|
|
4070
|
+
: Array.isArray(taskIds) && typeof date === 'string'
|
|
4071
|
+
? taskIds.map((moveTaskId, index) => ({ id: moveTaskId, scheduledDate: date, orderInDay: index }))
|
|
4072
|
+
: null;
|
|
4073
|
+
if (Array.isArray(inputMoves)) {
|
|
4074
|
+
const resolvedMoves = inputMoves.map((move, index) => ({
|
|
4075
|
+
...move,
|
|
4076
|
+
id: resolveTaskByIdentifierOrThrow(move?.taskId ?? move?.id).id,
|
|
4077
|
+
scheduledDate: move?.date ?? move?.scheduledDate,
|
|
4078
|
+
orderInDay: move?.order ?? move?.orderInDay ?? index,
|
|
4079
|
+
}));
|
|
4080
|
+
const result = core.reorderScheduledTasks(resolvedMoves, ACTIVE_WORKSPACE_ID);
|
|
4081
|
+
recordMcpWriteAudit('update_schedule', {
|
|
4082
|
+
operation: 'reorder',
|
|
4083
|
+
taskIds: resolvedMoves.map((m) => m.id),
|
|
4084
|
+
moveCount: resolvedMoves.length
|
|
4085
|
+
});
|
|
4086
|
+
await notifyResourceMutation({
|
|
4087
|
+
taskIds: resolvedMoves.map((m) => m.id),
|
|
4088
|
+
includeTasks: true,
|
|
4089
|
+
includeStats: true,
|
|
4090
|
+
});
|
|
4091
|
+
return {
|
|
4092
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4024
4095
|
const inputId = taskId ?? legacyInputId;
|
|
4025
|
-
const
|
|
4096
|
+
const scheduleInput = date !== undefined ? date : legacyScheduledDate;
|
|
4026
4097
|
const orderInDay = order ?? legacyOrderInDay;
|
|
4027
4098
|
const { id } = resolveTaskByIdentifierOrThrow(inputId);
|
|
4028
|
-
if (
|
|
4029
|
-
throw new Error('
|
|
4030
|
-
const result = core.updateTaskSchedule(id, { scheduledDate, orderInDay }, ACTIVE_WORKSPACE_ID);
|
|
4031
|
-
if (!result) {
|
|
4032
|
-
throw new Error(`Task ${id} not found`);
|
|
4099
|
+
if (scheduleInput === undefined) {
|
|
4100
|
+
throw new Error('date is required; pass null or an empty string to unschedule the task.');
|
|
4033
4101
|
}
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
type: "text",
|
|
4043
|
-
text: JSON.stringify({
|
|
4044
|
-
success: true,
|
|
4045
|
-
scheduling: {
|
|
4046
|
-
taskId: id,
|
|
4047
|
-
requestedDate: scheduledDate,
|
|
4048
|
-
persistedDate: result.task.scheduledDate,
|
|
4049
|
-
persistedWeekKey: result.task.scheduledWeekKey,
|
|
4050
|
-
persistedOrderInDay: result.task.orderInDay ?? null
|
|
4051
|
-
},
|
|
4052
|
-
warnings: result.warnings,
|
|
4053
|
-
task: buildTaskWriteSummary(result.task)
|
|
4054
|
-
}, null, 2)
|
|
4055
|
-
}],
|
|
4056
|
-
};
|
|
4057
|
-
}
|
|
4058
|
-
case "unschedule_task": {
|
|
4059
|
-
const { id: legacyInputId, taskId } = args;
|
|
4060
|
-
const inputId = taskId ?? legacyInputId;
|
|
4061
|
-
const { id } = resolveTaskByIdentifierOrThrow(inputId);
|
|
4062
|
-
const result = core.updateTaskSchedule(id, { scheduledDate: null, orderInDay: null }, ACTIVE_WORKSPACE_ID);
|
|
4102
|
+
const requestedDate = typeof scheduleInput === 'string' ? scheduleInput.trim() : scheduleInput;
|
|
4103
|
+
const isUnschedule = requestedDate === null || requestedDate === '';
|
|
4104
|
+
if (!isUnschedule && typeof requestedDate !== 'string') {
|
|
4105
|
+
throw new Error('date must be a string, null, or an empty string.');
|
|
4106
|
+
}
|
|
4107
|
+
const result = core.updateTaskSchedule(id, isUnschedule
|
|
4108
|
+
? { scheduledDate: null, orderInDay: null }
|
|
4109
|
+
: { scheduledDate: requestedDate, orderInDay }, ACTIVE_WORKSPACE_ID);
|
|
4063
4110
|
if (!result) {
|
|
4064
4111
|
throw new Error(`Task ${id} not found`);
|
|
4065
4112
|
}
|
|
4066
|
-
|
|
4113
|
+
const operation = isUnschedule ? 'unschedule' : 'schedule';
|
|
4114
|
+
recordMcpWriteAudit('update_schedule', {
|
|
4115
|
+
operation,
|
|
4116
|
+
taskId: id,
|
|
4117
|
+
scheduledDate: isUnschedule ? null : requestedDate,
|
|
4118
|
+
orderInDay: isUnschedule ? null : orderInDay ?? null
|
|
4119
|
+
});
|
|
4067
4120
|
await notifyResourceMutation({
|
|
4068
4121
|
taskIds: [id],
|
|
4069
4122
|
includeTasks: true,
|
|
@@ -4074,47 +4127,20 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4074
4127
|
type: "text",
|
|
4075
4128
|
text: JSON.stringify({
|
|
4076
4129
|
success: true,
|
|
4130
|
+
operation,
|
|
4077
4131
|
scheduling: {
|
|
4078
4132
|
taskId: id,
|
|
4079
|
-
requestedDate: null,
|
|
4133
|
+
requestedDate: isUnschedule ? null : requestedDate,
|
|
4080
4134
|
persistedDate: result.task.scheduledDate,
|
|
4081
4135
|
persistedWeekKey: result.task.scheduledWeekKey,
|
|
4082
4136
|
persistedOrderInDay: result.task.orderInDay ?? null
|
|
4083
4137
|
},
|
|
4084
4138
|
warnings: result.warnings,
|
|
4085
|
-
task: result.task
|
|
4139
|
+
task: buildTaskWriteSummary(result.task)
|
|
4086
4140
|
}, null, 2)
|
|
4087
4141
|
}],
|
|
4088
4142
|
};
|
|
4089
4143
|
}
|
|
4090
|
-
case "reorder_scheduled_tasks": {
|
|
4091
|
-
const { moves, date, taskIds } = args;
|
|
4092
|
-
const inputMoves = Array.isArray(moves)
|
|
4093
|
-
? moves
|
|
4094
|
-
: Array.isArray(taskIds) && typeof date === 'string'
|
|
4095
|
-
? taskIds.map((taskId, index) => ({ id: taskId, scheduledDate: date, orderInDay: index }))
|
|
4096
|
-
: null;
|
|
4097
|
-
if (!Array.isArray(inputMoves)) {
|
|
4098
|
-
throw new Error('moves must be an array');
|
|
4099
|
-
}
|
|
4100
|
-
const resolvedMoves = inputMoves.map((move) => ({
|
|
4101
|
-
...move,
|
|
4102
|
-
id: resolveTaskByIdentifierOrThrow(move?.taskId ?? move?.id).id
|
|
4103
|
-
}));
|
|
4104
|
-
const result = core.reorderScheduledTasks(resolvedMoves, ACTIVE_WORKSPACE_ID);
|
|
4105
|
-
recordMcpWriteAudit('reorder_scheduled_tasks', {
|
|
4106
|
-
taskIds: resolvedMoves.map((m) => m.id),
|
|
4107
|
-
moveCount: resolvedMoves.length
|
|
4108
|
-
});
|
|
4109
|
-
await notifyResourceMutation({
|
|
4110
|
-
taskIds: resolvedMoves.map((m) => m.id),
|
|
4111
|
-
includeTasks: true,
|
|
4112
|
-
includeStats: true,
|
|
4113
|
-
});
|
|
4114
|
-
return {
|
|
4115
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
4116
|
-
};
|
|
4117
|
-
}
|
|
4118
4144
|
case "bulk_carryover": {
|
|
4119
4145
|
const { fromWeek, toWeek, mode, includeStatuses = ['task', 'in-progress', 'review', 'on-hold'] } = args;
|
|
4120
4146
|
if (!fromWeek || typeof fromWeek !== 'string')
|
|
@@ -4425,96 +4451,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4425
4451
|
throw error;
|
|
4426
4452
|
}
|
|
4427
4453
|
}
|
|
4428
|
-
case "assign_task": {
|
|
4429
|
-
const { id: inputId, assignee } = args;
|
|
4430
|
-
const { id } = resolveTaskByIdentifierOrThrow(inputId);
|
|
4431
|
-
const resolvedAssignee = resolveRuntimeTaskAssigneeInput(assignee);
|
|
4432
|
-
if (!resolvedAssignee)
|
|
4433
|
-
throw new Error('assignee is required');
|
|
4434
|
-
const updated = core.updateTask(id, { assignee: resolvedAssignee }, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
|
|
4435
|
-
if (!updated)
|
|
4436
|
-
throw new Error(`Task ${id} not found`);
|
|
4437
|
-
recordMcpWriteAudit('assign_task', { taskId: id, assignee: resolvedAssignee });
|
|
4438
|
-
await notifyResourceMutation({
|
|
4439
|
-
taskIds: [id],
|
|
4440
|
-
includeTasks: true,
|
|
4441
|
-
includeStats: true,
|
|
4442
|
-
});
|
|
4443
|
-
return {
|
|
4444
|
-
content: [{
|
|
4445
|
-
type: "text",
|
|
4446
|
-
text: JSON.stringify({
|
|
4447
|
-
task: buildTaskWriteSummary(updated),
|
|
4448
|
-
assignee: resolvedAssignee,
|
|
4449
|
-
}, null, 2)
|
|
4450
|
-
}],
|
|
4451
|
-
};
|
|
4452
|
-
}
|
|
4453
|
-
case "bulk_assign_tasks": {
|
|
4454
|
-
const { ids, assignee } = args;
|
|
4455
|
-
const list = Array.isArray(ids) ? ids : [];
|
|
4456
|
-
if (list.length === 0)
|
|
4457
|
-
throw new Error('ids array is required');
|
|
4458
|
-
const resolvedAssignee = resolveRuntimeTaskAssigneeInput(assignee);
|
|
4459
|
-
if (!resolvedAssignee)
|
|
4460
|
-
throw new Error('assignee is required');
|
|
4461
|
-
const resolvedIds = list.map((id) => resolveTaskByIdentifierOrThrow(id).id);
|
|
4462
|
-
const updates = resolvedIds.map((id) => ({ id, assignee: resolvedAssignee }));
|
|
4463
|
-
const preview = core.bulkUpdateFieldsPreview(updates, ACTIVE_WORKSPACE_ID);
|
|
4464
|
-
const changed = [];
|
|
4465
|
-
const errors = [];
|
|
4466
|
-
for (const skipped of preview.skipped) {
|
|
4467
|
-
if (skipped.statusCode >= 400) {
|
|
4468
|
-
errors.push({
|
|
4469
|
-
index: skipped.index,
|
|
4470
|
-
id: skipped.id,
|
|
4471
|
-
message: skipped.message,
|
|
4472
|
-
statusCode: skipped.statusCode,
|
|
4473
|
-
code: skipped.code
|
|
4474
|
-
});
|
|
4475
|
-
}
|
|
4476
|
-
}
|
|
4477
|
-
for (const item of preview.toApply) {
|
|
4478
|
-
const updated = core.updateTask(item.id, item.fields, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
|
|
4479
|
-
if (!updated) {
|
|
4480
|
-
errors.push({ index: item.index, id: item.id, message: 'Task not found', statusCode: 404, code: 'TASK_NOT_FOUND' });
|
|
4481
|
-
continue;
|
|
4482
|
-
}
|
|
4483
|
-
changed.push({ id: updated.id, assignee: updated.assignee || 'unassigned' });
|
|
4484
|
-
}
|
|
4485
|
-
const summary = buildBatchSummary({
|
|
4486
|
-
operation: 'bulk-assign-tasks',
|
|
4487
|
-
total: resolvedIds.length,
|
|
4488
|
-
succeeded: changed.length
|
|
4489
|
-
});
|
|
4490
|
-
recordMcpWriteAudit('bulk_assign_tasks', {
|
|
4491
|
-
assignee: resolvedAssignee,
|
|
4492
|
-
requested: resolvedIds.length,
|
|
4493
|
-
changed: changed.length
|
|
4494
|
-
});
|
|
4495
|
-
await notifyResourceMutation({
|
|
4496
|
-
taskIds: resolvedIds,
|
|
4497
|
-
includeTasks: true,
|
|
4498
|
-
includeStats: true,
|
|
4499
|
-
});
|
|
4500
|
-
return {
|
|
4501
|
-
content: [{
|
|
4502
|
-
type: "text",
|
|
4503
|
-
text: JSON.stringify({
|
|
4504
|
-
success: errors.length === 0,
|
|
4505
|
-
summary,
|
|
4506
|
-
results: {
|
|
4507
|
-
assigned: changed,
|
|
4508
|
-
preflight: {
|
|
4509
|
-
deduped: preview.deduped,
|
|
4510
|
-
skipped_noop: preview.skipped.filter((s) => s.code === 'NO_CHANGES' || s.code === 'DUPLICATE_SUPERSEDED').length
|
|
4511
|
-
}
|
|
4512
|
-
},
|
|
4513
|
-
errors
|
|
4514
|
-
}, null, 2)
|
|
4515
|
-
}],
|
|
4516
|
-
};
|
|
4517
|
-
}
|
|
4518
4454
|
case "archive_task": {
|
|
4519
4455
|
const { id: inputId, status, reason } = args;
|
|
4520
4456
|
const { id, task } = resolveTaskByIdentifierOrThrow(inputId);
|
|
@@ -4531,15 +4467,33 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4531
4467
|
};
|
|
4532
4468
|
}
|
|
4533
4469
|
case "get_config": {
|
|
4470
|
+
const startedAtMs = Date.now();
|
|
4471
|
+
const categories = getActiveCategories();
|
|
4472
|
+
const types = getActiveTypes();
|
|
4473
|
+
const priorities = getActivePriorities();
|
|
4474
|
+
const approaches = getActiveApproaches();
|
|
4475
|
+
logMcpReadObservation('get_config', startedAtMs, {
|
|
4476
|
+
categoryCount: categories.length,
|
|
4477
|
+
typeCount: types.length,
|
|
4478
|
+
priorityCount: priorities.length,
|
|
4479
|
+
approachCount: approaches.length,
|
|
4480
|
+
activeToolProfile,
|
|
4481
|
+
});
|
|
4534
4482
|
return {
|
|
4535
4483
|
content: [{
|
|
4536
4484
|
type: "text",
|
|
4537
4485
|
text: JSON.stringify({
|
|
4538
|
-
categories
|
|
4539
|
-
types
|
|
4540
|
-
priorities
|
|
4541
|
-
approaches
|
|
4542
|
-
complexities: COMPLEXITY_OPTIONS
|
|
4486
|
+
categories,
|
|
4487
|
+
types,
|
|
4488
|
+
priorities,
|
|
4489
|
+
approaches,
|
|
4490
|
+
complexities: COMPLEXITY_OPTIONS,
|
|
4491
|
+
mcpToolProfile: {
|
|
4492
|
+
active: activeToolProfile,
|
|
4493
|
+
available: ['default', 'extended', 'full'],
|
|
4494
|
+
defaultForRuntime: defaultToolProfile,
|
|
4495
|
+
guidance: 'Cloud sessions default to the default profile. Local/dev sessions default to full. Use TASKFORCE_MCP_TOOL_PROFILE=default|extended|full for local stdio, or pass an explicit toolProfile runtime option where supported.',
|
|
4496
|
+
}
|
|
4543
4497
|
}, null, 2)
|
|
4544
4498
|
}],
|
|
4545
4499
|
};
|
|
@@ -5090,18 +5044,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5090
5044
|
case "delete_initiative_attachment": {
|
|
5091
5045
|
return deletePlanningAttachment('initiative', args);
|
|
5092
5046
|
}
|
|
5093
|
-
case "list_archive": {
|
|
5094
|
-
const { format = "markdown" } = args;
|
|
5095
|
-
const archive = core.listArchive(ACTIVE_WORKSPACE_ID);
|
|
5096
|
-
if (format === "json") {
|
|
5097
|
-
return {
|
|
5098
|
-
content: [{ type: "text", text: JSON.stringify(archive.archived, null, 2) }],
|
|
5099
|
-
};
|
|
5100
|
-
}
|
|
5101
|
-
return {
|
|
5102
|
-
content: [{ type: "text", text: renderTaskTable(archive.archived) }],
|
|
5103
|
-
};
|
|
5104
|
-
}
|
|
5105
5047
|
case "unarchive_task": {
|
|
5106
5048
|
const { id: inputId } = args;
|
|
5107
5049
|
const { id, task: existingTask } = resolveTaskByIdentifierOrThrow(inputId, { notFoundMessage: buildTaskLookupFailureMessage(String(inputId || '').trim()) });
|
|
@@ -5188,18 +5130,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5188
5130
|
content: [{ type: "text", text: "Task types updated" }],
|
|
5189
5131
|
};
|
|
5190
5132
|
}
|
|
5191
|
-
case "bulk_update_type": {
|
|
5192
|
-
const { fromType, toType } = args;
|
|
5193
|
-
const count = core.bulkUpdateType(fromType, toType, ACTIVE_WORKSPACE_ID);
|
|
5194
|
-
recordMcpWriteAudit('bulk_update_type', { fromType, toType, updated: count });
|
|
5195
|
-
await notifyResourceMutation({
|
|
5196
|
-
includeTasks: true,
|
|
5197
|
-
includeStats: true,
|
|
5198
|
-
});
|
|
5199
|
-
return {
|
|
5200
|
-
content: [{ type: "text", text: `Updated ${count} items from ${fromType} to ${toType}` }],
|
|
5201
|
-
};
|
|
5202
|
-
}
|
|
5203
5133
|
case "bulk_update_fields": {
|
|
5204
5134
|
const { updates } = args;
|
|
5205
5135
|
const list = Array.isArray(updates) ? updates : [];
|
|
@@ -5334,6 +5264,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5334
5264
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5335
5265
|
const { name, arguments: args } = request.params;
|
|
5336
5266
|
try {
|
|
5267
|
+
assertToolProfile(name);
|
|
5337
5268
|
assertToolPermission(name);
|
|
5338
5269
|
ensureResolvedProfileForTool(name, args);
|
|
5339
5270
|
assertResolvedProfileForMutatingTool(name);
|
|
@@ -5365,6 +5296,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5365
5296
|
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
5366
5297
|
tenantId,
|
|
5367
5298
|
version: MCP_SERVER_VERSION,
|
|
5299
|
+
toolProfile: activeToolProfile,
|
|
5368
5300
|
};
|
|
5369
5301
|
}
|
|
5370
5302
|
export async function startTaskforceMcpStdioServer(options = {}) {
|