@vibe-forge/mcp 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ import type {
2
+ AdapterErrorData,
3
+ AskUserQuestionParams,
4
+ PermissionInteractionContext,
5
+ PermissionInteractionDecision,
6
+ SessionPermissionMode
7
+ } from '@vibe-forge/types'
8
+ import { normalizePermissionToolName } from '@vibe-forge/utils'
9
+ import type { PermissionToolSubject } from '@vibe-forge/utils'
10
+
11
+ export { applyTaskPermissionDecision, syncTaskPermissionStateMirror } from './permission-state'
12
+
13
+ const PERMISSION_PROJECT_CONFIG_PATH = '.ai.config.json'
14
+ export const PERMISSION_DECISION_CANCEL = 'cancel'
15
+
16
+ const uniqueStrings = (values: string[]) => [...new Set(values)]
17
+
18
+ const normalizeKeys = (values: string[]) =>
19
+ uniqueStrings(
20
+ values
21
+ .map((value) => normalizePermissionToolName(value)?.key ?? value.trim())
22
+ .filter((value): value is string => value.trim() !== '')
23
+ )
24
+
25
+ const buildPermissionOption = (
26
+ label: string,
27
+ value: PermissionInteractionDecision,
28
+ description: string
29
+ ) => ({ label, value, description })
30
+
31
+ const resolvePermissionToolSubject = (value: string): PermissionToolSubject | undefined => (
32
+ normalizePermissionToolName(value)
33
+ )
34
+
35
+ export interface PermissionErrorContext {
36
+ subjectKeys: string[]
37
+ deniedTools: string[]
38
+ reasons: string[]
39
+ }
40
+
41
+ export const extractPermissionErrorContext = (error: AdapterErrorData): PermissionErrorContext => {
42
+ const details = error.details != null && typeof error.details === 'object'
43
+ ? error.details as Record<string, unknown>
44
+ : {}
45
+ const rawDeniedTools = new Set<string>()
46
+ const reasons = new Set<string>()
47
+
48
+ const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
49
+ for (const denial of permissionDenials) {
50
+ if (denial == null || typeof denial !== 'object') continue
51
+ const record = denial as Record<string, unknown>
52
+ if (typeof record.message === 'string' && record.message.trim() !== '') {
53
+ reasons.add(record.message.trim())
54
+ }
55
+ if (Array.isArray(record.deniedTools)) {
56
+ for (const tool of record.deniedTools) {
57
+ if (typeof tool === 'string' && tool.trim() !== '') {
58
+ rawDeniedTools.add(tool.trim())
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ if (Array.isArray(details.deniedTools)) {
65
+ for (const tool of details.deniedTools) {
66
+ if (typeof tool === 'string' && tool.trim() !== '') {
67
+ rawDeniedTools.add(tool.trim())
68
+ }
69
+ }
70
+ }
71
+
72
+ if (typeof details.toolName === 'string' && details.toolName.trim() !== '') {
73
+ rawDeniedTools.add(details.toolName.trim())
74
+ }
75
+
76
+ if (typeof error.message === 'string' && error.message.trim() !== '') {
77
+ reasons.add(error.message.trim())
78
+ }
79
+
80
+ const deniedTools = [...rawDeniedTools]
81
+ const subjectKeys = uniqueStrings(
82
+ deniedTools
83
+ .map(tool => resolvePermissionToolSubject(tool)?.key)
84
+ .filter((key): key is string => key != null && key.trim() !== '')
85
+ )
86
+
87
+ return {
88
+ subjectKeys,
89
+ deniedTools,
90
+ reasons: [...reasons]
91
+ }
92
+ }
93
+
94
+ export const resolvePermissionInteractionDecision = (
95
+ answer: string | string[]
96
+ ): PermissionInteractionDecision | typeof PERMISSION_DECISION_CANCEL | undefined => {
97
+ const normalizedAnswer = Array.isArray(answer) ? answer[0] : answer
98
+ if (typeof normalizedAnswer !== 'string') return undefined
99
+
100
+ const raw = normalizedAnswer.trim()
101
+ if (raw === '') return undefined
102
+ if (raw === PERMISSION_DECISION_CANCEL) return PERMISSION_DECISION_CANCEL
103
+
104
+ if (
105
+ raw === 'allow_once' ||
106
+ raw === 'allow_session' ||
107
+ raw === 'allow_project' ||
108
+ raw === 'deny_once' ||
109
+ raw === 'deny_session' ||
110
+ raw === 'deny_project'
111
+ ) {
112
+ return raw
113
+ }
114
+
115
+ return undefined
116
+ }
117
+
118
+ export const buildPermissionRecoveryPayload = (params: {
119
+ sessionId: string
120
+ adapter?: string
121
+ currentMode?: SessionPermissionMode
122
+ context: PermissionErrorContext
123
+ }): AskUserQuestionParams | undefined => {
124
+ const subjectKeys = normalizeKeys(params.context.subjectKeys)
125
+ if (subjectKeys.length === 0) {
126
+ return undefined
127
+ }
128
+
129
+ const primarySubjectKey = subjectKeys[0] ?? 'UnknownTool'
130
+ const subjectLabel = subjectKeys.length <= 1
131
+ ? primarySubjectKey
132
+ : `${subjectKeys[0]} 等 ${subjectKeys.length} 项工具`
133
+ const deniedTools = uniqueStrings([
134
+ ...params.context.deniedTools,
135
+ ...subjectKeys
136
+ ])
137
+ const permissionContext: PermissionInteractionContext = {
138
+ adapter: params.adapter,
139
+ currentMode: params.currentMode,
140
+ deniedTools,
141
+ reasons: uniqueStrings(params.context.reasons),
142
+ subjectKey: primarySubjectKey,
143
+ subjectLabel,
144
+ scope: 'tool',
145
+ projectConfigPath: PERMISSION_PROJECT_CONFIG_PATH
146
+ }
147
+
148
+ return {
149
+ sessionId: params.sessionId,
150
+ kind: 'permission',
151
+ question: subjectKeys.length <= 1
152
+ ? `当前任务需要使用 ${subjectLabel} 才能继续,请选择处理方式。`
153
+ : `当前任务涉及 ${subjectKeys.join('、')} 等工具,请选择处理方式。`,
154
+ options: [
155
+ buildPermissionOption('同意本次', 'allow_once', '仅继续这次被拦截的操作。'),
156
+ buildPermissionOption('同意并在当前会话忽略类似调用', 'allow_session', '本会话内同类工具不再重复询问。'),
157
+ buildPermissionOption(
158
+ '同意并在当前项目忽略类似调用',
159
+ 'allow_project',
160
+ `写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
161
+ ),
162
+ buildPermissionOption('拒绝本次', 'deny_once', '拒绝当前这次操作。'),
163
+ buildPermissionOption('拒绝并在当前会话阻止类似调用', 'deny_session', '本会话内同类工具直接拒绝。'),
164
+ buildPermissionOption(
165
+ '拒绝并在当前项目阻止类似调用',
166
+ 'deny_project',
167
+ `写入 ${PERMISSION_PROJECT_CONFIG_PATH},后续新会话仍生效。`
168
+ )
169
+ ],
170
+ permissionContext
171
+ }
172
+ }
@@ -0,0 +1,200 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { buildConfigJsonVariables, loadConfig, updateConfigFile } from '@vibe-forge/config'
6
+ import type { Config, PermissionInteractionDecision } from '@vibe-forge/types'
7
+ import {
8
+ normalizePermissionToolName,
9
+ normalizeSessionPermissionState,
10
+ resolvePermissionMirrorPath
11
+ } from '@vibe-forge/utils'
12
+ import type { SessionPermissionState } from '@vibe-forge/utils'
13
+
14
+ const PERMISSION_PROJECT_CONFIG_PATH = '.ai.config.json'
15
+
16
+ const uniqueStrings = (values: string[]) => [...new Set(values)]
17
+
18
+ const normalizeKeys = (values: string[]) =>
19
+ uniqueStrings(
20
+ values
21
+ .map((value) => normalizePermissionToolName(value)?.key ?? value.trim())
22
+ .filter((value): value is string => value.trim() !== '')
23
+ )
24
+
25
+ const removeKeys = (values: string[], keys: Set<string>) => (
26
+ values.filter((value) => {
27
+ const normalized = normalizePermissionToolName(value)?.key ?? value.trim()
28
+ return !keys.has(normalized)
29
+ })
30
+ )
31
+
32
+ const buildGeneralSectionValue = (config: Config | undefined, permissions: Config['permissions']) => ({
33
+ baseDir: config?.baseDir,
34
+ effort: config?.effort,
35
+ defaultAdapter: config?.defaultAdapter,
36
+ defaultModelService: config?.defaultModelService,
37
+ defaultModel: config?.defaultModel,
38
+ recommendedModels: config?.recommendedModels,
39
+ interfaceLanguage: config?.interfaceLanguage,
40
+ modelLanguage: config?.modelLanguage,
41
+ announcements: config?.announcements,
42
+ permissions,
43
+ env: config?.env,
44
+ notifications: config?.notifications,
45
+ shortcuts: config?.shortcuts
46
+ })
47
+
48
+ const mutateSessionPermissionState = (
49
+ state: SessionPermissionState,
50
+ keys: string[],
51
+ action: PermissionInteractionDecision
52
+ ): SessionPermissionState => {
53
+ const targetKeys = normalizeKeys(keys)
54
+ const keySet = new Set(targetKeys)
55
+ const next = normalizeSessionPermissionState(state)
56
+
57
+ switch (action) {
58
+ case 'allow_once':
59
+ next.onceAllow = uniqueStrings([...removeKeys(next.onceAllow, keySet), ...targetKeys])
60
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
61
+ return next
62
+ case 'allow_session':
63
+ next.allow = uniqueStrings([...removeKeys(next.allow, keySet), ...targetKeys])
64
+ next.deny = removeKeys(next.deny, keySet)
65
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
66
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
67
+ return next
68
+ case 'allow_project':
69
+ next.allow = uniqueStrings([...removeKeys(next.allow, keySet), ...targetKeys])
70
+ next.deny = removeKeys(next.deny, keySet)
71
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
72
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
73
+ return next
74
+ case 'deny_once':
75
+ return next
76
+ case 'deny_session':
77
+ next.deny = uniqueStrings([...removeKeys(next.deny, keySet), ...targetKeys])
78
+ next.allow = removeKeys(next.allow, keySet)
79
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
80
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
81
+ return next
82
+ case 'deny_project':
83
+ next.deny = uniqueStrings([...removeKeys(next.deny, keySet), ...targetKeys])
84
+ next.allow = removeKeys(next.allow, keySet)
85
+ next.onceAllow = removeKeys(next.onceAllow, keySet)
86
+ next.onceDeny = removeKeys(next.onceDeny, keySet)
87
+ return next
88
+ }
89
+ }
90
+
91
+ const loadTaskConfig = async (cwd: string) =>
92
+ await loadConfig({
93
+ cwd,
94
+ jsonVariables: buildConfigJsonVariables(cwd, process.env)
95
+ })
96
+
97
+ const buildMergedProjectPermissions = async (cwd: string) => {
98
+ const [projectConfig, userConfig] = await loadTaskConfig(cwd)
99
+ return {
100
+ allow: [
101
+ ...(projectConfig?.permissions?.allow ?? []),
102
+ ...(userConfig?.permissions?.allow ?? [])
103
+ ],
104
+ deny: [
105
+ ...(projectConfig?.permissions?.deny ?? []),
106
+ ...(userConfig?.permissions?.deny ?? [])
107
+ ],
108
+ ask: [
109
+ ...(projectConfig?.permissions?.ask ?? []),
110
+ ...(userConfig?.permissions?.ask ?? [])
111
+ ]
112
+ }
113
+ }
114
+
115
+ const updateProjectPermissionLists = async (
116
+ cwd: string,
117
+ keys: string[],
118
+ target: 'allow' | 'deny'
119
+ ) => {
120
+ const targetKeys = normalizeKeys(keys)
121
+ const keySet = new Set(targetKeys)
122
+ const [projectConfig] = await loadTaskConfig(cwd)
123
+ const existingPermissions = projectConfig?.permissions ?? {}
124
+ const nextPermissions: Config['permissions'] = {
125
+ ...existingPermissions,
126
+ allow: removeKeys(existingPermissions.allow ?? [], keySet),
127
+ deny: removeKeys(existingPermissions.deny ?? [], keySet),
128
+ ask: removeKeys(existingPermissions.ask ?? [], keySet)
129
+ }
130
+ nextPermissions[target] = uniqueStrings([...(nextPermissions[target] ?? []), ...targetKeys])
131
+
132
+ await updateConfigFile({
133
+ workspaceFolder: cwd,
134
+ source: 'project',
135
+ section: 'general',
136
+ value: buildGeneralSectionValue(projectConfig, nextPermissions)
137
+ })
138
+ }
139
+
140
+ export const syncTaskPermissionStateMirror = async (params: {
141
+ cwd: string
142
+ adapter?: string
143
+ sessionId: string
144
+ permissionState: SessionPermissionState
145
+ }) => {
146
+ if (params.adapter !== 'claude-code' && params.adapter !== 'opencode') {
147
+ return
148
+ }
149
+
150
+ const mirrorPath = resolvePermissionMirrorPath(params.cwd, params.adapter, params.sessionId)
151
+ const projectPermissions = await buildMergedProjectPermissions(params.cwd)
152
+ await mkdir(dirname(mirrorPath), { recursive: true })
153
+ await writeFile(
154
+ mirrorPath,
155
+ `${
156
+ JSON.stringify(
157
+ {
158
+ sessionId: params.sessionId,
159
+ adapter: params.adapter,
160
+ permissionState: normalizeSessionPermissionState(params.permissionState),
161
+ projectPermissions,
162
+ updatedAt: Date.now()
163
+ },
164
+ null,
165
+ 2
166
+ )
167
+ }\n`,
168
+ 'utf8'
169
+ )
170
+ }
171
+
172
+ export const applyTaskPermissionDecision = async (params: {
173
+ cwd: string
174
+ sessionId: string
175
+ adapter?: string
176
+ permissionState: SessionPermissionState
177
+ subjectKeys: string[]
178
+ action: PermissionInteractionDecision
179
+ }) => {
180
+ const subjectKeys = normalizeKeys(params.subjectKeys)
181
+ if (subjectKeys.length === 0) {
182
+ return normalizeSessionPermissionState(params.permissionState)
183
+ }
184
+
185
+ if (params.action === 'allow_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'allow')
186
+ if (params.action === 'deny_project') await updateProjectPermissionLists(params.cwd, subjectKeys, 'deny')
187
+
188
+ const nextState = mutateSessionPermissionState(
189
+ params.permissionState,
190
+ subjectKeys,
191
+ params.action
192
+ )
193
+ await syncTaskPermissionStateMirror({
194
+ cwd: params.cwd,
195
+ adapter: params.adapter,
196
+ sessionId: params.sessionId,
197
+ permissionState: nextState
198
+ })
199
+ return nextState
200
+ }
@@ -0,0 +1,95 @@
1
+ import process from 'node:process'
2
+
3
+ import type { SessionPermissionMode } from '@vibe-forge/types'
4
+
5
+ import type { TaskInfo } from './manager'
6
+
7
+ export const SESSION_PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'] as const
8
+
9
+ export const START_TASKS_DESCRIPTION =
10
+ 'Start multiple tasks in background or foreground. If a task stalls, fails, or asks for permission/input, call GetTaskInfo. If GetTaskInfo returns pendingInput or pendingInteraction, resolve it with SubmitTaskInput. If logs show permission_required, you can answer the recovery prompt with SubmitTaskInput instead of restarting manually.'
11
+
12
+ export const GET_TASK_INFO_DESCRIPTION =
13
+ 'Get the detailed status, logs, pendingInput, pendingInteraction, lastError, and guidance for a task. Use this when a task seems stuck, is waiting for permission/input, or has failed. If pendingInput is present, answer it with SubmitTaskInput.'
14
+
15
+ export const SUBMIT_TASK_INPUT_DESCRIPTION =
16
+ 'Submit input for a task that is blocked waiting for permission or user input. First call GetTaskInfo or ListTasks, then use taskId plus one of pendingInput.payload.options[].value when available. Common permission answers are allow_once, allow_session, allow_project, deny_once, deny_session, or deny_project.'
17
+
18
+ export const RESPOND_TASK_INTERACTION_DESCRIPTION =
19
+ 'Deprecated alias of SubmitTaskInput. Use SubmitTaskInput for both permission prompts and generic task input.'
20
+
21
+ export const STOP_TASK_DESCRIPTION =
22
+ 'Stop a running or blocked task. Use this when the task is no longer needed or cannot recover cleanly.'
23
+
24
+ export const LIST_TASKS_DESCRIPTION =
25
+ 'List all managed tasks with status, pendingInput, pendingInteraction, lastError, and guidance. Use this first to find which tasks are blocked; then call GetTaskInfo for one task or SubmitTaskInput if it is waiting for input.'
26
+
27
+ export const TASK_PERMISSION_MODE_DESCRIPTION =
28
+ 'Permission mode for the task. If omitted, inherits the parent session. Raise it only when the task is blocked by permission errors.'
29
+
30
+ export const TASK_BACKGROUND_DESCRIPTION =
31
+ 'Whether to run in background (default: true). If false, waits until the task completes, fails, or becomes blocked waiting for input, then returns the current logs.'
32
+
33
+ export const resolveInheritedPermissionMode = (): SessionPermissionMode | undefined => {
34
+ const value = process.env.__VF_PROJECT_AI_PERMISSION_MODE__?.trim()
35
+ if (value == null || value === '') return undefined
36
+ return (SESSION_PERMISSION_MODES as readonly string[]).includes(value)
37
+ ? value as SessionPermissionMode
38
+ : undefined
39
+ }
40
+
41
+ const buildTaskGuidance = (task: {
42
+ status?: string
43
+ pendingInteraction?: { payload?: { options?: Array<{ label: string; value?: string }> } }
44
+ lastError?: { code?: string }
45
+ }) => {
46
+ const hints: string[] = []
47
+
48
+ if (task.pendingInteraction != null) {
49
+ const optionValues = task.pendingInteraction.payload?.options
50
+ ?.map(option => option.value ?? option.label)
51
+ .filter((value): value is string => value.trim() !== '')
52
+ hints.push(
53
+ optionValues != null && optionValues.length > 0
54
+ ? `Task is waiting for input. Call SubmitTaskInput with one of: ${optionValues.join(', ')}.`
55
+ : 'Task is waiting for input. Call SubmitTaskInput with the desired answer.'
56
+ )
57
+ }
58
+
59
+ if (task.lastError?.code === 'permission_required') {
60
+ hints.push(
61
+ 'Task hit a permission error. Retry StartTasks with a more suitable permissionMode, or update project permissions before retrying.'
62
+ )
63
+ }
64
+
65
+ if (task.status === 'failed' && hints.length === 0) {
66
+ hints.push('Task failed. Inspect logs and lastError, then restart the task if needed.')
67
+ }
68
+
69
+ return hints
70
+ }
71
+
72
+ export const serializeTaskInfo = (params: {
73
+ taskId: string
74
+ description?: string
75
+ status?: TaskInfo['status']
76
+ info?: TaskInfo
77
+ }) => {
78
+ const info = params.info
79
+ const safeInfo = (() => {
80
+ if (info == null) {
81
+ return undefined
82
+ }
83
+ const { session, onStop, serverSync, createdAt, ...rest } = info
84
+ return rest
85
+ })()
86
+ return {
87
+ taskId: params.taskId,
88
+ description: params.description ?? info?.description,
89
+ status: info?.status ?? params.status,
90
+ logs: info?.logs ?? [],
91
+ pendingInput: safeInfo?.pendingInteraction,
92
+ ...safeInfo,
93
+ guidance: buildTaskGuidance(safeInfo ?? {})
94
+ }
95
+ }
@@ -0,0 +1,145 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { Register } from '../types'
4
+ import type { TaskManager } from './manager'
5
+ import {
6
+ GET_TASK_INFO_DESCRIPTION,
7
+ LIST_TASKS_DESCRIPTION,
8
+ RESPOND_TASK_INTERACTION_DESCRIPTION,
9
+ STOP_TASK_DESCRIPTION,
10
+ SUBMIT_TASK_INPUT_DESCRIPTION,
11
+ serializeTaskInfo
12
+ } from './presentation'
13
+
14
+ export const registerTaskRuntimeTools = (
15
+ server: Parameters<Register>[0],
16
+ taskManager: TaskManager
17
+ ) => {
18
+ server.registerTool(
19
+ 'GetTaskInfo',
20
+ {
21
+ title: 'Get Task Info',
22
+ description: GET_TASK_INFO_DESCRIPTION,
23
+ inputSchema: z.object({
24
+ taskId: z.string().describe('The ID of the task to check')
25
+ })
26
+ },
27
+ async ({ taskId }) => {
28
+ const task = taskManager.getTask(taskId)
29
+ if (!task) {
30
+ return {
31
+ content: [{ type: 'text', text: `Task ${taskId} not found.` }],
32
+ isError: true
33
+ }
34
+ }
35
+ return {
36
+ content: [{
37
+ type: 'text',
38
+ text: JSON.stringify([serializeTaskInfo({ taskId, info: task })])
39
+ }]
40
+ }
41
+ }
42
+ )
43
+
44
+ server.registerTool(
45
+ 'SubmitTaskInput',
46
+ {
47
+ title: 'Submit Task Input',
48
+ description: SUBMIT_TASK_INPUT_DESCRIPTION,
49
+ inputSchema: z.object({
50
+ taskId: z.string().describe('The ID of the task that is waiting for input'),
51
+ interactionId: z
52
+ .string()
53
+ .describe('Optional interaction ID. Omit it when the task only has one pending input.')
54
+ .optional(),
55
+ data: z
56
+ .union([z.string(), z.array(z.string()).min(1)])
57
+ .describe('The input to submit. Prefer pendingInput.payload.options[].value when available.')
58
+ })
59
+ },
60
+ async ({ taskId, interactionId, data }) => {
61
+ await taskManager.submitTaskInput({
62
+ taskId,
63
+ interactionId,
64
+ data
65
+ })
66
+ const task = taskManager.getTask(taskId)
67
+ return {
68
+ content: [{
69
+ type: 'text',
70
+ text: JSON.stringify([serializeTaskInfo({ taskId, info: task })])
71
+ }]
72
+ }
73
+ }
74
+ )
75
+
76
+ server.registerTool(
77
+ 'RespondTaskInteraction',
78
+ {
79
+ title: 'Respond Task Interaction',
80
+ description: RESPOND_TASK_INTERACTION_DESCRIPTION,
81
+ inputSchema: z.object({
82
+ taskId: z.string().describe('The ID of the task that is waiting for input'),
83
+ interactionId: z
84
+ .string()
85
+ .describe('Optional interaction ID. Omit it when the task only has one pending interaction.')
86
+ .optional(),
87
+ response: z
88
+ .union([z.string(), z.array(z.string()).min(1)])
89
+ .describe('The selected answer. Prefer pendingInteraction.options[].value when available.')
90
+ })
91
+ },
92
+ async ({ taskId, interactionId, response }) => {
93
+ await taskManager.submitTaskInput({
94
+ taskId,
95
+ interactionId,
96
+ data: response
97
+ })
98
+ const task = taskManager.getTask(taskId)
99
+ return {
100
+ content: [{
101
+ type: 'text',
102
+ text: JSON.stringify([serializeTaskInfo({ taskId, info: task })])
103
+ }]
104
+ }
105
+ }
106
+ )
107
+
108
+ server.registerTool(
109
+ 'StopTask',
110
+ {
111
+ title: 'Stop Task',
112
+ description: STOP_TASK_DESCRIPTION,
113
+ inputSchema: z.object({
114
+ taskId: z.string().describe('The ID of the task to stop')
115
+ })
116
+ },
117
+ async ({ taskId }) => {
118
+ const success = taskManager.stopTask(taskId)
119
+ return {
120
+ content: [{
121
+ type: 'text',
122
+ text: success ? `Task ${taskId} stopped.` : `Failed to stop task ${taskId} (not found or already stopped).`
123
+ }]
124
+ }
125
+ }
126
+ )
127
+
128
+ server.registerTool(
129
+ 'ListTasks',
130
+ {
131
+ title: 'List Tasks',
132
+ description: LIST_TASKS_DESCRIPTION,
133
+ inputSchema: z.object({})
134
+ },
135
+ async () => {
136
+ const tasks = taskManager.getAllTasks()
137
+ return {
138
+ content: [{
139
+ type: 'text',
140
+ text: JSON.stringify(tasks.map(task => serializeTaskInfo({ taskId: task.taskId, info: task })))
141
+ }]
142
+ }
143
+ }
144
+ )
145
+ }