Skip to content

Commit e9c0ac2

Browse files
committed
Fix mcp setting application
Signed-off-by: James Ostrander <jostrand@redhat.com>
1 parent 1059f0e commit e9c0ac2

10 files changed

Lines changed: 130 additions & 38 deletions

File tree

.vscode/launch.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
1717
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
1818
"preLaunchTask": "npm: build"
19+
},
20+
{
21+
"name": "Run Extension (kserve workspace)",
22+
"type": "extensionHost",
23+
"request": "launch",
24+
"args": [
25+
"--extensionDevelopmentPath=${workspaceFolder}",
26+
"/home/jostrand/projects/kserve"
27+
],
28+
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
29+
"preLaunchTask": "npm: build"
1930
}
2031
]
2132
}

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,30 @@ Each launch configuration defined in your workspace becomes a tool named `launch
9898

9999
### ⚙️ MCP Options
100100

101-
Tasks and launch configurations support an optional `mcp` block for MCP-specific settings. Hover over options in your editor for full documentation.
101+
Tasks support MCP options inside the `options` block. Launch configurations use them at the top level (you'll see a schema warning, but it works correctly). Hover over options in your editor for full documentation.
102102

103+
**Task example:**
103104
```json
104105
{
105106
"label": "Build",
106107
"type": "shell",
107108
"command": "npm run build",
108-
"mcp": {
109-
"returnOutput": "onFailure",
110-
"interactive": true
109+
"options": {
110+
"mcp": { "returnOutput": "onFailure", "interactive": true }
111111
}
112112
}
113113
```
114114

115+
**Launch example:**
116+
```json
117+
{
118+
"name": "Debug",
119+
"type": "node",
120+
"request": "launch",
121+
"mcp": { "preserveConsole": true }
122+
}
123+
```
124+
115125
| Option | Default | Description |
116126
|--------|---------|-------------|
117127
| `returnOutput` | `onFailure` | When to include output: `always`, `onFailure`, `never` |

esbuild.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const buildOptions = {
1212
target: 'node18',
1313
sourcemap: true,
1414
minify: !isWatch,
15+
// Prefer ESM modules over CommonJS/UMD to ensure static imports are used.
16+
// This fixes bundling issues with jsonc-parser which has dynamic requires in its UMD build.
17+
mainFields: ['module', 'main'],
1518
};
1619

1720
async function build() {

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@
6464
"title": "Ignition MCP",
6565
"properties": {
6666
"ignition-mcp.outputLimit": {
67-
"type": ["integer", "null"],
67+
"type": [
68+
"integer",
69+
"null"
70+
],
6871
"default": 20480,
6972
"minimum": 1024,
7073
"description": "Default maximum characters to capture from task and debug output. Can be overridden per-task/launch via mcp.outputLimit in tasks.json/launch.json. Set to null for unlimited. Default is 20480 (20KB)."
@@ -98,6 +101,7 @@
98101
},
99102
"dependencies": {
100103
"@modelcontextprotocol/sdk": "^1.0.0",
104+
"jsonc-parser": "^3.3.1",
101105
"zod": "^3.22.0"
102106
}
103107
}

schemas/launch-mcp.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"properties": {
1010
"mcp": {
1111
"type": "object",
12-
"description": "MCP-specific options for Ignition MCP extension. Controls how AI assistants interact with this debug configuration.",
12+
"description": "MCP-specific options for Ignition MCP extension. Note: VS Code may show a schema warning for this property, but it works correctly.",
1313
"properties": {
1414
"returnOutput": {
1515
"type": "string",

schemas/tasks-mcp.schema.json

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,34 @@
77
"items": {
88
"type": "object",
99
"properties": {
10-
"mcp": {
10+
"options": {
1111
"type": "object",
12-
"description": "MCP-specific options for Ignition MCP extension. Controls how AI assistants interact with this task.",
1312
"properties": {
14-
"returnOutput": {
15-
"type": "string",
16-
"enum": ["always", "onFailure", "never"],
17-
"default": "onFailure",
18-
"markdownDescription": "Controls when terminal output is included in MCP tool responses.\n\n- `always` - Always include full terminal output\n- `onFailure` - Only include output when exit code != 0 (default)\n- `never` - Never include output; use `get_task_output` to retrieve separately\n\nHelps manage AI context size: successful builds don't need hundreds of lines of output, but failed builds should include error details."
19-
},
20-
"outputLimit": {
21-
"type": ["integer", "null"],
22-
"minimum": 1024,
23-
"default": 20480,
24-
"markdownDescription": "Maximum characters to capture from task output. Default is 20480 (20KB). Set to `null` for unlimited. Overrides the global `ignition-mcp.outputLimit` setting."
25-
},
26-
"interactive": {
27-
"type": "boolean",
28-
"default": false,
29-
"markdownDescription": "Run task in VS Code's native terminal with full TTY support for interactive input.\n\n**Use when:**\n- Task runs `sudo` commands requiring password input\n- Task prompts for user confirmation (y/n)\n- Commands use `read` or similar interactive input\n- Any command requiring TTY support\n\n**Trade-off:** Output cannot be captured in this mode. The MCP response will show `[Interactive mode: output not captured]`. The terminal will open with focus so the user can interact."
13+
"mcp": {
14+
"type": "object",
15+
"description": "MCP-specific options for Ignition MCP extension. Controls how AI assistants interact with this task.",
16+
"properties": {
17+
"returnOutput": {
18+
"type": "string",
19+
"enum": ["always", "onFailure", "never"],
20+
"default": "onFailure",
21+
"markdownDescription": "Controls when terminal output is included in MCP tool responses.\n\n- `always` - Always include full terminal output\n- `onFailure` - Only include output when exit code != 0 (default)\n- `never` - Never include output; use `get_task_output` to retrieve separately\n\nHelps manage AI context size: successful builds don't need hundreds of lines of output, but failed builds should include error details."
22+
},
23+
"outputLimit": {
24+
"type": ["integer", "null"],
25+
"minimum": 1024,
26+
"default": 20480,
27+
"markdownDescription": "Maximum characters to capture from task output. Default is 20480 (20KB). Set to `null` for unlimited. Overrides the global `ignition-mcp.outputLimit` setting."
28+
},
29+
"interactive": {
30+
"type": "boolean",
31+
"default": false,
32+
"markdownDescription": "Run task in VS Code's native terminal with full TTY support for interactive input.\n\n**Use when:**\n- Task runs `sudo` commands requiring password input\n- Task prompts for user confirmation (y/n)\n- Commands use `read` or similar interactive input\n- Any command requiring TTY support\n\n**Trade-off:** Output cannot be captured in this mode. The MCP response will show `[Interactive mode: output not captured]`. The terminal will open with focus so the user can interact."
33+
}
34+
},
35+
"additionalProperties": false
3036
}
31-
},
32-
"additionalProperties": false
37+
}
3338
}
3439
}
3540
}

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export async function activate(context: vscode.ExtensionContext) {
2626
context.subscriptions.push(outputChannel);
2727
log('Ignition MCP extension activating...');
2828
taskManager = new TaskManager();
29+
taskManager.setOutputChannel(outputChannel);
2930
launchManager = new LaunchManager();
3031
context.subscriptions.push(taskManager);
3132
context.subscriptions.push(launchManager);
@@ -125,6 +126,9 @@ async function enableServer() {
125126
try {
126127
log(`Starting MCP server on port ${currentPort}...`);
127128
mcpServer = new MCPServer(taskManager, launchManager, currentPort, handleShutdownRequested);
129+
if (outputChannel) {
130+
mcpServer.setOutputChannel(outputChannel);
131+
}
128132
await mcpServer.start();
129133
serverState = 'running';
130134
updateStatusBar();

src/server/mcpServer.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as http from 'http';
2+
import * as vscode from 'vscode';
23
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
34
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
45
import { z } from 'zod';
@@ -17,6 +18,7 @@ export class MCPServer {
1718
private taskManager: TaskManager;
1819
private launchManager: LaunchManager;
1920
private onShutdownRequested?: ShutdownCallback;
21+
private outputChannel: vscode.OutputChannel | null = null;
2022

2123
constructor(
2224
taskManager: TaskManager,
@@ -34,6 +36,17 @@ export class MCPServer {
3436
});
3537
}
3638

39+
setOutputChannel(channel: vscode.OutputChannel) {
40+
this.outputChannel = channel;
41+
}
42+
43+
private log(message: string) {
44+
const timestamp = new Date().toLocaleTimeString();
45+
const fullMessage = `[${timestamp}] [MCPServer] ${message}`;
46+
this.outputChannel?.appendLine(fullMessage);
47+
console.log(fullMessage);
48+
}
49+
3750
private sanitizeToolName(name: string, prefix: string): string {
3851
return prefix + name
3952
.toLowerCase()
@@ -289,7 +302,7 @@ export class MCPServer {
289302
for (const task of tasks) {
290303
this.registerTaskTool(task);
291304
}
292-
console.log(`Registered ${tasks.length} task tools`);
305+
this.log(`Registered ${tasks.length} task tools`);
293306
}
294307

295308
private buildInputSchema(inputs: InputDefinition[]): Record<string, z.ZodOptional<z.ZodString>> {
@@ -481,7 +494,7 @@ export class MCPServer {
481494
for (const config of configs) {
482495
this.registerLaunchTool(config);
483496
}
484-
console.log(`Registered ${configs.length} launch tools`);
497+
this.log(`Registered ${configs.length} launch tools`);
485498
}
486499

487500
private registerLaunchTool(config: LaunchInfo) {
@@ -593,7 +606,7 @@ export class MCPServer {
593606
});
594607
// Explicitly listen on 0.0.0.0 to ensure both IPv4 and IPv6 work via loopback
595608
this.server.listen(this.port, '0.0.0.0', () => {
596-
console.log(`MCP server listening on port ${this.port}`);
609+
this.log(`MCP server listening on port ${this.port}`);
597610
resolve();
598611
});
599612
});
@@ -608,7 +621,7 @@ export class MCPServer {
608621
try {
609622
await this.transport.handleRequest(req, res);
610623
} catch (error) {
611-
console.error('Error handling MCP request:', error);
624+
this.log(`Error handling MCP request: ${error}`);
612625
if (!res.headersSent) {
613626
res.writeHead(500);
614627
res.end('Internal server error');

src/tasks/taskManager.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
22
import * as fs from 'fs';
33
import * as path from 'path';
44
import * as cp from 'child_process';
5+
import * as jsonc from 'jsonc-parser';
56

67
export interface TaskInputDefinition {
78
id: string;
@@ -137,6 +138,7 @@ export class TaskManager implements vscode.Disposable {
137138
private executionToId: Map<vscode.TaskExecution, string> = new Map();
138139
private interactiveExecutionIds: Set<string> = new Set();
139140
private disposables: vscode.Disposable[] = [];
141+
private outputChannel: vscode.OutputChannel | null = null;
140142

141143
constructor() {
142144
this.disposables.push(
@@ -146,6 +148,17 @@ export class TaskManager implements vscode.Disposable {
146148
);
147149
}
148150

151+
setOutputChannel(channel: vscode.OutputChannel) {
152+
this.outputChannel = channel;
153+
}
154+
155+
private log(message: string) {
156+
const timestamp = new Date().toLocaleTimeString();
157+
const fullMessage = `[${timestamp}] [TaskManager] ${message}`;
158+
this.outputChannel?.appendLine(fullMessage);
159+
console.log(fullMessage);
160+
}
161+
149162
private getDefaultOutputLimit(): number | null {
150163
const config = vscode.workspace.getConfiguration('ignition-mcp');
151164
return config.get<number | null>('outputLimit', 20480);
@@ -167,18 +180,32 @@ export class TaskManager implements vscode.Disposable {
167180
}
168181

169182
async listTasks(): Promise<TaskInfo[]> {
183+
this.log(`listTasks called`);
170184
const tasks = await vscode.tasks.fetchTasks();
185+
this.log(`Found ${tasks.length} VS Code tasks`);
171186
const tasksJsonData = this.readTasksJson();
172187
const inputDefinitions = tasksJsonData?.inputs || [];
173188
const rawTasks = tasksJsonData?.tasks || [];
189+
this.log(`Read ${rawTasks.length} tasks from tasks.json`);
174190
return tasks.map((task) => {
175191
const rawCommand = this.getRawCommand(task);
176192
const usedInputIds = this.findInputReferences(rawCommand);
177193
const inputs = usedInputIds.length > 0
178194
? inputDefinitions.filter((inp: TaskInputDefinition) => usedInputIds.includes(inp.id))
179195
: undefined;
180196
const rawTaskEntry = rawTasks.find((t) => t.label === task.name);
181-
const mcpOptions = rawTaskEntry?.mcp;
197+
const mcpOptions = rawTaskEntry?.options?.mcp;
198+
if (mcpOptions) {
199+
this.log(`Task "${task.name}" has mcpOptions: ${JSON.stringify(mcpOptions)}`);
200+
} else if (task.name === 'CRC Refresh') {
201+
this.log(`Task "CRC Refresh" found but NO mcpOptions. rawTaskEntry: ${JSON.stringify(rawTaskEntry)}`);
202+
this.log(`Task object keys: ${Object.keys(task)}`);
203+
this.log(`Task definition: ${JSON.stringify(task.definition)}`);
204+
const exec = task.execution;
205+
if (exec && 'options' in exec) {
206+
this.log(`Execution options: ${JSON.stringify((exec as { options?: unknown }).options)}`);
207+
}
208+
}
182209
return {
183210
name: task.name,
184211
source: task.source,
@@ -200,21 +227,33 @@ export class TaskManager implements vscode.Disposable {
200227

201228
private readTasksJson(): {
202229
inputs?: TaskInputDefinition[];
203-
tasks?: Array<{ label?: string; mcp?: McpOptions }>
230+
tasks?: Array<{ label?: string; options?: { mcp?: McpOptions } }>
204231
} | null {
205232
const workspaceFolders = vscode.workspace.workspaceFolders;
206233
if (!workspaceFolders || workspaceFolders.length === 0) {
234+
this.log('readTasksJson: No workspace folders');
207235
return null;
208236
}
209237
const tasksJsonPath = path.join(workspaceFolders[0].uri.fsPath, '.vscode', 'tasks.json');
210238
if (!fs.existsSync(tasksJsonPath)) {
239+
this.log(`readTasksJson: tasks.json not found at ${tasksJsonPath}`);
211240
return null;
212241
}
213242
try {
214243
const content = fs.readFileSync(tasksJsonPath, 'utf-8');
215-
const cleanedContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
216-
return JSON.parse(cleanedContent);
217-
} catch {
244+
const errors: jsonc.ParseError[] = [];
245+
const parsed = jsonc.parse(content, errors, { allowTrailingComma: true });
246+
if (errors.length > 0) {
247+
this.log(`readTasksJson: JSONC parse warnings: ${errors.map(e => jsonc.printParseErrorCode(e.error)).join(', ')}`);
248+
}
249+
this.log(`readTasksJson: Successfully parsed tasks.json from ${tasksJsonPath}`);
250+
const tasksWithMcp = parsed?.tasks?.filter((t: { options?: { mcp?: McpOptions } }) => t.options?.mcp);
251+
if (tasksWithMcp?.length > 0) {
252+
this.log(`Tasks with mcp options: ${tasksWithMcp.map((t: { label?: string }) => t.label).join(', ')}`);
253+
}
254+
return parsed;
255+
} catch (err) {
256+
this.log(`readTasksJson: Error parsing tasks.json: ${err}`);
218257
return null;
219258
}
220259
}
@@ -259,6 +298,7 @@ export class TaskManager implements vscode.Disposable {
259298
}
260299

261300
async runTask(taskName: string, inputValues?: Record<string, string>, mcpOptions?: McpOptions): Promise<TaskRunResult> {
301+
this.log(`runTask called for "${taskName}" with mcpOptions: ${JSON.stringify(mcpOptions)}`);
262302
const tasks = await vscode.tasks.fetchTasks();
263303
const task = tasks.find((t) => t.name === taskName);
264304
if (!task) {
@@ -278,8 +318,10 @@ export class TaskManager implements vscode.Disposable {
278318
this.outputLimits.set(executionId, mcpOptions.outputLimit);
279319
}
280320
if (mcpOptions?.interactive) {
321+
this.log(`Running task "${taskName}" in INTERACTIVE mode`);
281322
return this.runTaskInteractive(task, executionId, inputValues);
282323
}
324+
this.log(`Running task "${taskName}" in CAPTURE mode`);
283325
let rawCommand = this.getRawCommand(task);
284326
if (inputValues && Object.keys(inputValues).length > 0) {
285327
for (const [inputId, value] of Object.entries(inputValues)) {

0 commit comments

Comments
 (0)