@rawwee/interactive-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env bun
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import notifier from 'node-notifier';
5
+ import yargs from 'yargs';
6
+ import { hideBin } from 'yargs/helpers';
7
+ import { getCmdWindowInput } from './commands/input/index.js';
8
+ import { startIntensiveChatSession, askQuestionInSession, stopIntensiveChatSession, } from './commands/intensive-chat/index.js';
9
+ import { USER_INPUT_TIMEOUT_SECONDS, USER_INPUT_TIMEOUT_SENTINEL, } from './constants.js';
10
+ import logger from './utils/logger.js';
11
+ import { validateRepositoryBaseDirectory } from './utils/base-directory.js';
12
+ // Import tool definitions using the new structure
13
+ import { requestUserInputTool } from './tool-definitions/request-user-input.js';
14
+ import { messageCompleteNotificationTool } from './tool-definitions/message-complete-notification.js';
15
+ import { intensiveChatTools } from './tool-definitions/intensive-chat.js';
16
+ const allToolCapabilities = {
17
+ request_user_input: requestUserInputTool.capability,
18
+ message_complete_notification: messageCompleteNotificationTool.capability,
19
+ start_intensive_chat: intensiveChatTools.start.capability,
20
+ ask_intensive_chat: intensiveChatTools.ask.capability,
21
+ stop_intensive_chat: intensiveChatTools.stop.capability,
22
+ };
23
+ const argv = yargs(hideBin(process.argv))
24
+ .option('timeout', {
25
+ alias: 't',
26
+ type: 'number',
27
+ description: 'Default timeout for user input prompts in seconds',
28
+ default: USER_INPUT_TIMEOUT_SECONDS,
29
+ })
30
+ .option('disable-tools', {
31
+ alias: 'd',
32
+ type: 'string',
33
+ description: 'Comma-separated list of tool names to disable. Available options: request_user_input, message_complete_notification, intensive_chat (disables all intensive chat tools).',
34
+ default: '',
35
+ })
36
+ .help()
37
+ .alias('help', 'h')
38
+ .parseSync();
39
+ const globalTimeoutSeconds = argv.timeout;
40
+ const disabledTools = argv['disable-tools']
41
+ .split(',')
42
+ .map((tool) => tool.trim())
43
+ .filter(Boolean);
44
+ logger.info({
45
+ globalTimeoutSeconds,
46
+ disabledTools,
47
+ }, 'Interactive MCP server configuration loaded.');
48
+ // Store active intensive chat sessions
49
+ const activeChatSessions = new Map();
50
+ // --- Filter Capabilities Based on Args ---
51
+ // Helper function to check if a tool is effectively disabled (directly or via group)
52
+ const isToolDisabled = (toolName) => {
53
+ if (disabledTools.includes(toolName)) {
54
+ return true;
55
+ }
56
+ if ([
57
+ // Check if tool belongs to the intensive_chat group and the group is disabled
58
+ 'start_intensive_chat',
59
+ 'ask_intensive_chat',
60
+ 'stop_intensive_chat',
61
+ ].includes(toolName) &&
62
+ disabledTools.includes('intensive_chat')) {
63
+ return true;
64
+ }
65
+ return false;
66
+ };
67
+ // Create a new object with only the enabled tool capabilities
68
+ const enabledToolCapabilities = Object.fromEntries(Object.entries(allToolCapabilities).filter(([toolName]) => {
69
+ return !isToolDisabled(toolName);
70
+ })); // Assert type after filtering
71
+ // --- End Filter Capabilities Based on Args ---
72
+ // Helper function to check if a tool should be registered (used later)
73
+ const isToolEnabled = (toolName) => {
74
+ // A tool is enabled if it's present in the filtered capabilities
75
+ return toolName in enabledToolCapabilities;
76
+ };
77
+ // Initialize MCP server with FILTERED capabilities
78
+ const server = new McpServer({
79
+ name: 'Interactive MCP',
80
+ version: '1.0.0',
81
+ capabilities: {
82
+ tools: enabledToolCapabilities, // Use the filtered capabilities
83
+ },
84
+ });
85
+ // Conditionally register tools based on command-line arguments
86
+ if (isToolEnabled('request_user_input')) {
87
+ // Use properties from the imported tool object
88
+ server.tool('request_user_input',
89
+ // Need to handle description potentially being a function
90
+ typeof requestUserInputTool.description === 'function'
91
+ ? requestUserInputTool.description(globalTimeoutSeconds)
92
+ : requestUserInputTool.description, requestUserInputTool.schema, // Use schema property
93
+ async (args) => {
94
+ // Use inferred args type
95
+ const { projectName, message, predefinedOptions, baseDirectory } = args;
96
+ try {
97
+ const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
98
+ const promptMessage = `${projectName}: ${message}`;
99
+ const answer = await getCmdWindowInput(projectName, promptMessage, globalTimeoutSeconds, true, validatedBaseDirectory, predefinedOptions);
100
+ // Check for the specific timeout indicator
101
+ if (answer === USER_INPUT_TIMEOUT_SENTINEL) {
102
+ return {
103
+ content: [
104
+ { type: 'text', text: 'User did not reply: Timeout occurred.' },
105
+ ],
106
+ };
107
+ }
108
+ // Empty string means user submitted empty input, non-empty is actual reply
109
+ else if (answer === '') {
110
+ return {
111
+ content: [{ type: 'text', text: 'User replied with empty input.' }],
112
+ };
113
+ }
114
+ else {
115
+ const reply = `User replied: ${answer}`;
116
+ return { content: [{ type: 'text', text: reply }] };
117
+ }
118
+ }
119
+ catch (error) {
120
+ const errorMessage = error instanceof Error
121
+ ? `Failed to request user input: ${error.message}`
122
+ : 'Failed to request user input: unknown error.';
123
+ return { content: [{ type: 'text', text: errorMessage }] };
124
+ }
125
+ });
126
+ }
127
+ if (isToolEnabled('message_complete_notification')) {
128
+ // Use properties from the imported tool object
129
+ server.tool('message_complete_notification',
130
+ // Description is a string here, but handle consistently
131
+ typeof messageCompleteNotificationTool.description === 'function'
132
+ ? messageCompleteNotificationTool.description(globalTimeoutSeconds) // Should not happen based on definition, but safe
133
+ : messageCompleteNotificationTool.description, messageCompleteNotificationTool.schema, // Use schema property
134
+ (args) => {
135
+ // Use inferred args type
136
+ const { projectName, message } = args;
137
+ notifier.notify({ title: projectName, message });
138
+ return {
139
+ content: [
140
+ {
141
+ type: 'text',
142
+ text: 'Notification sent. You can now wait for user input.',
143
+ },
144
+ ],
145
+ };
146
+ });
147
+ }
148
+ // --- Intensive Chat Tool Registrations ---
149
+ // Each tool must be checked individually based on filtered capabilities
150
+ if (isToolEnabled('start_intensive_chat')) {
151
+ // Use properties from the imported intensiveChatTools object
152
+ server.tool('start_intensive_chat',
153
+ // Description is a function here
154
+ typeof intensiveChatTools.start.description === 'function'
155
+ ? intensiveChatTools.start.description(globalTimeoutSeconds)
156
+ : intensiveChatTools.start.description, intensiveChatTools.start.schema, // Use schema property
157
+ async (args) => {
158
+ // Use inferred args type
159
+ const { sessionTitle, baseDirectory } = args;
160
+ try {
161
+ const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
162
+ // Start a new intensive chat session, passing global timeout
163
+ const sessionId = await startIntensiveChatSession(sessionTitle, validatedBaseDirectory, globalTimeoutSeconds);
164
+ // Track this session for the client
165
+ activeChatSessions.set(sessionId, sessionTitle);
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: `Intensive chat session started successfully. Session ID: ${sessionId}`,
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ catch (error) {
176
+ let errorMessage = 'Failed to start intensive chat session.';
177
+ if (error instanceof Error) {
178
+ errorMessage = `Failed to start intensive chat session: ${error.message}`;
179
+ }
180
+ else if (typeof error === 'string') {
181
+ errorMessage = `Failed to start intensive chat session: ${error}`;
182
+ }
183
+ return {
184
+ content: [
185
+ {
186
+ type: 'text',
187
+ text: errorMessage,
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ });
193
+ }
194
+ if (isToolEnabled('ask_intensive_chat')) {
195
+ // Use properties from the imported intensiveChatTools object
196
+ server.tool('ask_intensive_chat',
197
+ // Description is a string here
198
+ typeof intensiveChatTools.ask.description === 'function'
199
+ ? intensiveChatTools.ask.description(globalTimeoutSeconds) // Should not happen, but safe
200
+ : intensiveChatTools.ask.description, intensiveChatTools.ask.schema, // Use schema property
201
+ async (args) => {
202
+ // Use inferred args type
203
+ const { sessionId, question, predefinedOptions, baseDirectory } = args;
204
+ // Check if session exists
205
+ if (!activeChatSessions.has(sessionId)) {
206
+ return {
207
+ content: [
208
+ { type: 'text', text: 'Error: Invalid or expired session ID.' },
209
+ ],
210
+ };
211
+ }
212
+ try {
213
+ const validatedBaseDirectory = await validateRepositoryBaseDirectory(baseDirectory);
214
+ // Ask the question in the session
215
+ const answer = await askQuestionInSession(sessionId, question, validatedBaseDirectory, predefinedOptions);
216
+ // Check for the specific timeout indicator
217
+ if (answer === USER_INPUT_TIMEOUT_SENTINEL) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: 'User did not reply to question in intensive chat: Timeout occurred.',
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ else if (answer === null) {
228
+ return {
229
+ content: [
230
+ {
231
+ type: 'text',
232
+ text: 'User closed intensive chat session before replying.',
233
+ },
234
+ ],
235
+ };
236
+ }
237
+ // Empty string means user submitted empty input, non-empty is actual reply
238
+ else if (answer === '') {
239
+ return {
240
+ content: [
241
+ {
242
+ type: 'text',
243
+ text: 'User replied with empty input in intensive chat.',
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ else {
249
+ return {
250
+ content: [{ type: 'text', text: `User replied: ${answer}` }],
251
+ };
252
+ }
253
+ }
254
+ catch (error) {
255
+ let errorMessage = 'Failed to ask question in session.';
256
+ if (error instanceof Error) {
257
+ errorMessage = `Failed to ask question in session: ${error.message}`;
258
+ }
259
+ else if (typeof error === 'string') {
260
+ errorMessage = `Failed to ask question in session: ${error}`;
261
+ }
262
+ return {
263
+ content: [
264
+ {
265
+ type: 'text',
266
+ text: errorMessage,
267
+ },
268
+ ],
269
+ };
270
+ }
271
+ });
272
+ }
273
+ if (isToolEnabled('stop_intensive_chat')) {
274
+ // Use properties from the imported intensiveChatTools object
275
+ server.tool('stop_intensive_chat',
276
+ // Description is a string here
277
+ typeof intensiveChatTools.stop.description === 'function'
278
+ ? intensiveChatTools.stop.description(globalTimeoutSeconds) // Should not happen, but safe
279
+ : intensiveChatTools.stop.description, intensiveChatTools.stop.schema, // Use schema property
280
+ async (args) => {
281
+ // Use inferred args type
282
+ const { sessionId } = args;
283
+ // Check if session exists
284
+ if (!activeChatSessions.has(sessionId)) {
285
+ return {
286
+ content: [
287
+ { type: 'text', text: 'Error: Invalid or expired session ID.' },
288
+ ],
289
+ };
290
+ }
291
+ try {
292
+ // Stop the session
293
+ const success = await stopIntensiveChatSession(sessionId);
294
+ // Remove session from map if successful
295
+ if (success) {
296
+ activeChatSessions.delete(sessionId);
297
+ }
298
+ const message = success
299
+ ? 'Session stopped successfully.'
300
+ : 'Session not found or already stopped.';
301
+ return { content: [{ type: 'text', text: message }] };
302
+ }
303
+ catch (error) {
304
+ let errorMessage = 'Failed to stop intensive chat session.';
305
+ if (error instanceof Error) {
306
+ errorMessage = `Failed to stop intensive chat session: ${error.message}`;
307
+ }
308
+ else if (typeof error === 'string') {
309
+ errorMessage = `Failed to stop intensive chat session: ${error}`;
310
+ }
311
+ return { content: [{ type: 'text', text: errorMessage }] };
312
+ }
313
+ });
314
+ }
315
+ // --- End Intensive Chat Tool Registrations ---
316
+ // Run the server over stdio
317
+ const transport = new StdioServerTransport();
318
+ await server.connect(transport);
@@ -0,0 +1,236 @@
1
+ import { z } from 'zod';
2
+ // === Start Intensive Chat Definition ===
3
+ const startCapability = {
4
+ description: 'Start a persistent OpenTUI intensive chat session for gathering multiple answers quickly.',
5
+ parameters: {
6
+ type: 'object',
7
+ properties: {
8
+ sessionTitle: {
9
+ type: 'string',
10
+ description: 'Title for the intensive chat session',
11
+ },
12
+ baseDirectory: {
13
+ type: 'string',
14
+ description: 'Required absolute path to the current repository root (must be a git repo root; default autocomplete/search scope for this session)',
15
+ },
16
+ },
17
+ required: ['sessionTitle', 'baseDirectory'],
18
+ },
19
+ };
20
+ const startDescription = (globalTimeoutSeconds) => `<description>
21
+ Start an intensive chat session (OpenTUI terminal UI) for gathering multiple answers quickly from the user.
22
+ **Highly recommended** for scenarios requiring a sequence of related inputs or confirmations.
23
+ Very useful for gathering multiple answers from the user in a short period of time.
24
+ Especially useful for brainstorming ideas or discussing complex topics with the user.
25
+ </description>
26
+
27
+ <importantNotes>
28
+ - (!important!) Opens a persistent console window that stays open for multiple questions.
29
+ - (!important!) Returns a session ID that **must** be used for subsequent questions via 'ask_intensive_chat'.
30
+ - (!important!) **Must** be closed with 'stop_intensive_chat' when finished gathering all inputs.
31
+ - (!important!) After starting a session, **immediately** continue asking all necessary questions using 'ask_intensive_chat' within the **same response message**. Do not end the response until the chat is closed with 'stop_intensive_chat'. This creates a seamless conversational flow for the user.
32
+ </importantNotes>
33
+
34
+ <whenToUseThisTool>
35
+ - When you need to collect a series of quick answers from the user (more than 2-3 questions)
36
+ - When setting up a project with multiple configuration options
37
+ - When guiding a user through a multi-step process requiring input at each stage
38
+ - When gathering sequential user preferences
39
+ - When you want to maintain context between multiple related questions efficiently
40
+ - When brainstorming ideas with the user interactively
41
+ </whenToUseThisTool>
42
+
43
+ <features>
44
+ - Opens a persistent OpenTUI window for continuous interaction
45
+ - Renders markdown prompts, including code/diff snippets, for richer question context
46
+ - Supports option mode + free-text mode while asking follow-up questions
47
+ - Configurable timeout for each question (set via -t/--timeout, defaults to ${globalTimeoutSeconds} seconds)
48
+ - Returns a session ID for subsequent interactions
49
+ - Keeps full chat history visible to the user
50
+ - Maintains state between questions
51
+ - Requires baseDirectory and pins autocomplete/search scope to the repository root
52
+ </features>
53
+
54
+ <bestPractices>
55
+ - Use a descriptive session title related to the task
56
+ - Start with a clear initial question when possible
57
+ - Do not ask the question if you have another tool that can answer the question
58
+ - e.g. when you searching file in the current repository, do not ask the question "Do you want to search for a file in the current repository?"
59
+ - e.g. prefer to use other tools to find the answer (Cursor tools or other MCP Server tools)
60
+ - Always store the returned session ID for later use
61
+ - Always close the session when you're done with stop_intensive_chat
62
+ </bestPractices>
63
+
64
+ <parameters>
65
+ - sessionTitle: Title for the intensive chat session (appears at the top of the console)
66
+ - baseDirectory: Required absolute path to the current repository root (must be a git repo root)
67
+ </parameters>
68
+
69
+ <examples>
70
+ - Start session for project setup: { "sessionTitle": "Project Configuration", "baseDirectory": "/workspace/project" }
71
+ - Start session with repository root scope: { "sessionTitle": "Project Configuration", "baseDirectory": "/workspace/project" }
72
+ </examples>`;
73
+ const startSchema = {
74
+ sessionTitle: z.string().describe('Title for the intensive chat session'),
75
+ baseDirectory: z
76
+ .string()
77
+ .describe('Required absolute path to the current repository root (must be a git repo root; default autocomplete/search scope for this session)'),
78
+ };
79
+ const startToolDefinition = {
80
+ capability: startCapability,
81
+ description: startDescription,
82
+ schema: startSchema,
83
+ };
84
+ // === Ask Intensive Chat Definition ===
85
+ const askCapability = {
86
+ description: 'Ask a question in an active OpenTUI intensive chat session.',
87
+ parameters: {
88
+ type: 'object',
89
+ properties: {
90
+ sessionId: {
91
+ type: 'string',
92
+ description: 'ID of the intensive chat session',
93
+ },
94
+ question: {
95
+ type: 'string',
96
+ description: 'Question to ask the user',
97
+ },
98
+ predefinedOptions: {
99
+ type: 'array',
100
+ items: { type: 'string' },
101
+ optional: true,
102
+ description: 'Predefined options for the user to choose from (optional)',
103
+ },
104
+ baseDirectory: {
105
+ type: 'string',
106
+ description: 'Required absolute path to the current repository root (must be a git repo root; autocomplete/search scope for this question)',
107
+ },
108
+ },
109
+ required: ['sessionId', 'question', 'baseDirectory'],
110
+ },
111
+ };
112
+ const askDescription = `<description>
113
+ Ask a new question in an active intensive chat session previously started with 'start_intensive_chat'.
114
+ </description>
115
+
116
+ <importantNotes>
117
+ - (!important!) Requires a valid session ID from 'start_intensive_chat'.
118
+ - (!important!) Supports predefined options for quick selection.
119
+ - (!important!) Returns the user's answer or indicates if they didn't respond.
120
+ - (!important!) **Use this repeatedly within the same response message** after 'start_intensive_chat' until all questions are asked.
121
+ </importantNotes>
122
+
123
+ <whenToUseThisTool>
124
+ - When continuing a series of questions in an intensive chat session.
125
+ - When you need the next piece of information in a multi-step process initiated via 'start_intensive_chat'.
126
+ - When offering multiple choice options to the user within the session.
127
+ - When gathering sequential information from the user within the session.
128
+ </whenToUseThisTool>
129
+
130
+ <features>
131
+ - Adds a new question to an existing chat session
132
+ - Supports predefined options for quick selection
133
+ - Returns the user's response
134
+ - Maintains the chat history in the console
135
+ - Requires baseDirectory for each question and scopes autocomplete/search to the repository root
136
+ </features>
137
+
138
+ <bestPractices>
139
+ - Ask one clear question at a time
140
+ - Provide predefined options when applicable
141
+ - Don't ask overly complex questions
142
+ - Keep questions focused on a single piece of information
143
+ </bestPractices>
144
+
145
+ <parameters>
146
+ - sessionId: ID of the intensive chat session (from start_intensive_chat)
147
+ - question: The question text to display to the user
148
+ - predefinedOptions: Array of predefined options for the user to choose from (optional)
149
+ - baseDirectory: Required absolute path to the current repository root (must be a git repo root)
150
+ </parameters>
151
+
152
+ <examples>
153
+ - Simple question: { "sessionId": "abcd1234", "question": "What is your project named?", "baseDirectory": "/workspace/project" }
154
+ - With predefined options: { "sessionId": "abcd1234", "question": "Would you like to use TypeScript?", "predefinedOptions": ["Yes", "No"], "baseDirectory": "/workspace/project" }
155
+ - Ask another repo-scoped question: { "sessionId": "abcd1234", "question": "Pick a file", "baseDirectory": "/workspace/project" }
156
+ </examples>`;
157
+ const askSchema = {
158
+ sessionId: z.string().describe('ID of the intensive chat session'),
159
+ question: z.string().describe('Question to ask the user'),
160
+ predefinedOptions: z
161
+ .array(z.string())
162
+ .optional()
163
+ .describe('Predefined options for the user to choose from (optional)'),
164
+ baseDirectory: z
165
+ .string()
166
+ .describe('Required absolute path to the current repository root (must be a git repo root; autocomplete/search scope for this question)'),
167
+ };
168
+ const askToolDefinition = {
169
+ capability: askCapability,
170
+ description: askDescription,
171
+ schema: askSchema,
172
+ };
173
+ // === Stop Intensive Chat Definition ===
174
+ const stopCapability = {
175
+ description: 'Stop and close an active intensive chat session.',
176
+ parameters: {
177
+ type: 'object',
178
+ properties: {
179
+ sessionId: {
180
+ type: 'string',
181
+ description: 'ID of the intensive chat session to stop',
182
+ },
183
+ },
184
+ required: ['sessionId'],
185
+ },
186
+ };
187
+ const stopDescription = `<description>
188
+ Stop and close an active intensive chat session. **Must be called** after all questions have been asked using 'ask_intensive_chat'.
189
+ </description>
190
+
191
+ <importantNotes>
192
+ - (!important!) Closes the console window for the intensive chat.
193
+ - (!important!) Frees up system resources.
194
+ - (!important!) **Should always be called** as the final step when finished with an intensive chat session, typically at the end of the response message where 'start_intensive_chat' was called.
195
+ </importantNotes>
196
+
197
+ <whenToUseThisTool>
198
+ - When you've completed gathering all needed information via 'ask_intensive_chat'.
199
+ - When the multi-step process requiring intensive chat is complete.
200
+ - When you're ready to move on to processing the collected information.
201
+ - When the user indicates they want to end the session (if applicable).
202
+ - As the final action related to the intensive chat flow within a single response message.
203
+ </whenToUseThisTool>
204
+
205
+ <features>
206
+ - Gracefully closes the console window
207
+ - Cleans up system resources
208
+ - Marks the session as complete
209
+ </features>
210
+
211
+ <bestPractices>
212
+ - Always stop sessions when you're done to free resources
213
+ - Provide a summary of the information collected before stopping
214
+ </bestPractices>
215
+
216
+ <parameters>
217
+ - sessionId: ID of the intensive chat session to stop
218
+ </parameters>
219
+
220
+ <examples>
221
+ - { "sessionId": "abcd1234" }
222
+ </examples>`;
223
+ const stopSchema = {
224
+ sessionId: z.string().describe('ID of the intensive chat session to stop'),
225
+ };
226
+ const stopToolDefinition = {
227
+ capability: stopCapability,
228
+ description: stopDescription,
229
+ schema: stopSchema,
230
+ };
231
+ // === Export Combined Intensive Chat Definitions ===
232
+ export const intensiveChatTools = {
233
+ start: startToolDefinition,
234
+ ask: askToolDefinition,
235
+ stop: stopToolDefinition,
236
+ };
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ // Define capability conforming to ToolCapabilityInfo
3
+ const capabilityInfo = {
4
+ description: 'Notify when a response has completed via OS notification.',
5
+ parameters: {
6
+ type: 'object',
7
+ properties: {
8
+ projectName: {
9
+ type: 'string',
10
+ description: 'Identifies the context/project making the notification (appears in notification title)',
11
+ },
12
+ message: {
13
+ type: 'string',
14
+ description: 'The specific notification text (appears in the body)',
15
+ },
16
+ },
17
+ required: ['projectName', 'message'],
18
+ },
19
+ };
20
+ // Define description conforming to ToolRegistrationDescription
21
+ const registrationDescription = `<description>
22
+ Notify when a response has completed. Use this tool **once** at the end of **each and every** message to signal completion to the user.
23
+ </description>
24
+
25
+ <importantNotes>
26
+ - (!important!) **MANDATORY:** ONLY use this tool exactly once per message to signal completion. **Do not forget this step.**
27
+ </importantNotes>
28
+
29
+ <whenToUseThisTool>
30
+ - When you've completed answering a user's query
31
+ - When you've finished executing a task or a sequence of tool calls
32
+ - When a multi-step process is complete
33
+ - When you want to provide a summary of completed actions just before ending the response
34
+ </whenToUseThisTool>
35
+
36
+ <features>
37
+ - Cross-platform OS notifications (Windows, macOS, Linux)
38
+ - Reusable tool to signal end of message
39
+ - Should be called exactly once per LLM response
40
+ </features>
41
+
42
+ <bestPractices>
43
+ - Keep messages concise
44
+ - Use projectName consistently to group notifications by context
45
+ </bestPractices>
46
+
47
+ <parameters>
48
+ - projectName: Identifies the context/project making the notification (appears in notification title)
49
+ - message: The specific notification text (appears in the body)
50
+ </parameters>
51
+
52
+ <examples>
53
+ - { "projectName": "MyApp", "message": "Feature implementation complete. All tests passing." }
54
+ - { "projectName": "MyLib", "message": "Analysis complete: 3 issues found and fixed." }
55
+ </examples>`;
56
+ // Define the Zod schema (as a raw shape object)
57
+ const rawSchema = {
58
+ projectName: z.string().describe('Notification title'),
59
+ message: z.string().describe('Notification body'),
60
+ };
61
+ // Combine into a single ToolDefinition object
62
+ export const messageCompleteNotificationTool = {
63
+ capability: capabilityInfo,
64
+ description: registrationDescription,
65
+ schema: rawSchema, // Use the raw shape here
66
+ };