@hileeon/mcc 0.1.8 → 0.1.9

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.
Files changed (86) hide show
  1. package/README.md +226 -127
  2. package/dist/accounts/store.d.ts +1 -0
  3. package/dist/accounts/store.d.ts.map +1 -1
  4. package/dist/accounts/store.js.map +1 -1
  5. package/dist/commands/launch.d.ts +9 -0
  6. package/dist/commands/launch.d.ts.map +1 -0
  7. package/dist/commands/launch.js +158 -0
  8. package/dist/commands/launch.js.map +1 -0
  9. package/dist/commands/mcp.d.ts +9 -0
  10. package/dist/commands/mcp.d.ts.map +1 -0
  11. package/dist/commands/mcp.js +112 -0
  12. package/dist/commands/mcp.js.map +1 -0
  13. package/dist/commands/profile.d.ts +8 -0
  14. package/dist/commands/profile.d.ts.map +1 -0
  15. package/dist/commands/profile.js +125 -0
  16. package/dist/commands/profile.js.map +1 -0
  17. package/dist/core/model-router.d.ts.map +1 -1
  18. package/dist/core/model-router.js +5 -2
  19. package/dist/core/model-router.js.map +1 -1
  20. package/dist/{dashboard-server.d.ts → dashboard/server.d.ts} +1 -1
  21. package/dist/dashboard/server.d.ts.map +1 -0
  22. package/dist/{dashboard-server.js → dashboard/server.js} +169 -51
  23. package/dist/dashboard/server.js.map +1 -0
  24. package/dist/mcc.d.ts +4 -2
  25. package/dist/mcc.d.ts.map +1 -1
  26. package/dist/mcc.js +121 -408
  27. package/dist/mcc.js.map +1 -1
  28. package/dist/mcp/mcp-config.d.ts +17 -1
  29. package/dist/mcp/mcp-config.d.ts.map +1 -1
  30. package/dist/mcp/mcp-config.js +50 -17
  31. package/dist/mcp/mcp-config.js.map +1 -1
  32. package/dist/proxy/proxy-daemon.d.ts.map +1 -1
  33. package/dist/proxy/proxy-daemon.js +17 -2
  34. package/dist/proxy/proxy-daemon.js.map +1 -1
  35. package/dist/proxy/proxy-entry.js +5 -3
  36. package/dist/proxy/proxy-entry.js.map +1 -1
  37. package/dist/proxy/proxy-server.d.ts.map +1 -1
  38. package/dist/proxy/proxy-server.js +32 -6
  39. package/dist/proxy/proxy-server.js.map +1 -1
  40. package/dist/shared/config.d.ts +15 -0
  41. package/dist/shared/config.d.ts.map +1 -0
  42. package/dist/shared/config.js +79 -0
  43. package/dist/shared/config.js.map +1 -0
  44. package/dist/shared/logger.d.ts +23 -18
  45. package/dist/shared/logger.d.ts.map +1 -1
  46. package/dist/shared/logger.js +17 -178
  47. package/dist/shared/logger.js.map +1 -1
  48. package/dist/shared/provider-preset-catalog.d.ts +6 -2
  49. package/dist/shared/provider-preset-catalog.d.ts.map +1 -1
  50. package/dist/shared/provider-preset-catalog.js +47 -26
  51. package/dist/shared/provider-preset-catalog.js.map +1 -1
  52. package/dist/ui/assets/index-ClqmrjNk.js +40 -0
  53. package/dist/ui/assets/index-CwMwQ-Z4.css +1 -0
  54. package/dist/ui/index.html +21 -13
  55. package/dist/update.d.ts +31 -0
  56. package/dist/update.d.ts.map +1 -0
  57. package/dist/update.js +196 -0
  58. package/dist/update.js.map +1 -0
  59. package/lib/mcp/mcc-image-analysis-server.cjs +454 -454
  60. package/lib/mcp/mcc-websearch-server.cjs +339 -339
  61. package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -510
  62. package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -526
  63. package/lib/mcp-hooks/websearch-transformer.cjs +1597 -1421
  64. package/lib/proxy/config/config-loader-facade.js +24 -24
  65. package/lib/proxy/glmt/delta-accumulator.js +362 -362
  66. package/lib/proxy/glmt/glmt-transformer.js +203 -203
  67. package/lib/proxy/glmt/index.js +40 -40
  68. package/lib/proxy/glmt/locale-enforcer.js +68 -68
  69. package/lib/proxy/glmt/pipeline/content-transformer.js +161 -161
  70. package/lib/proxy/glmt/pipeline/index.js +19 -19
  71. package/lib/proxy/glmt/pipeline/request-transformer.js +115 -115
  72. package/lib/proxy/glmt/pipeline/response-builder.js +204 -204
  73. package/lib/proxy/glmt/pipeline/stream-parser.js +233 -233
  74. package/lib/proxy/glmt/pipeline/tool-call-handler.js +77 -77
  75. package/lib/proxy/glmt/pipeline/types.js +5 -5
  76. package/lib/proxy/glmt/reasoning-enforcer.js +150 -150
  77. package/lib/proxy/glmt/sse-parser.js +101 -101
  78. package/lib/proxy/services/logging.js +13 -13
  79. package/lib/proxy/transformers/request-transformer.js +471 -471
  80. package/lib/proxy/transformers/sse-stream-transformer.js +198 -198
  81. package/lib/shared/logger.cjs +156 -138
  82. package/package.json +58 -41
  83. package/dist/dashboard-server.d.ts.map +0 -1
  84. package/dist/dashboard-server.js.map +0 -1
  85. package/dist/ui/assets/index-B16lhKZ6.js +0 -40
  86. package/dist/ui/assets/index-jEfiB6-h.css +0 -1
@@ -1,454 +1,454 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function loadRuntimeModule() {
7
- const candidates = [
8
- path.join(__dirname, 'image-analysis-runtime.cjs'),
9
- path.join(__dirname, '../mcp-hooks/image-analysis-runtime.cjs'),
10
- ];
11
-
12
- for (const candidate of candidates) {
13
- if (fs.existsSync(candidate)) {
14
- return require(candidate);
15
- }
16
- }
17
-
18
- throw new Error(
19
- `mcc-image-analysis runtime not found. Checked: ${candidates.map((candidate) => path.basename(candidate)).join(', ')}`
20
- );
21
- }
22
-
23
- const { analyzeFile, isAnalyzableFile } = loadRuntimeModule();
24
-
25
- const PROTOCOL_VERSION = '2024-11-05';
26
- const SERVER_NAME = 'mcc-image-analysis';
27
- const SERVER_VERSION = '1.0.0';
28
- const TOOL_NAME = 'ImageAnalysis';
29
- const TOOL_ALIASES = ['AnalyzeImage', 'ReadImage'];
30
- const TEMPLATE_NAMES = ['default', 'screenshot', 'document'];
31
- const TOOL_DESCRIPTION =
32
- 'Analyze a local image or PDF file with MCC provider-backed vision. Prefer this tool over Read for image and PDF paths. Use Read for text, code, and other plain files.';
33
-
34
- // Shared logger — set MCC_LOG_LEVEL=debug to enable
35
- const log = require('../shared/logger.cjs');
36
-
37
- function isSupportedToolName(name) {
38
- return name === TOOL_NAME || TOOL_ALIASES.includes(name);
39
- }
40
-
41
- function shouldExposeTools() {
42
- return (
43
- process.env.MCC_IMAGE_ANALYSIS_ENABLED === '1' &&
44
- process.env.MCC_IMAGE_ANALYSIS_SKIP !== '1' &&
45
- Boolean(process.env.MCC_CURRENT_PROVIDER || process.env.MCC_IMAGE_ANALYSIS_MODEL)
46
- );
47
- }
48
-
49
- function getTools() {
50
- if (!shouldExposeTools()) {
51
- return [];
52
- }
53
-
54
- return [
55
- {
56
- name: TOOL_NAME,
57
- description: TOOL_DESCRIPTION,
58
- inputSchema: {
59
- type: 'object',
60
- properties: {
61
- filePath: {
62
- type: 'string',
63
- description:
64
- 'Workspace-relative path, or an absolute path inside the current workspace, to a local image or PDF file to analyze.',
65
- },
66
- focus: {
67
- type: 'string',
68
- description:
69
- 'Optional question or area of focus, for example "explain the error dialog" or "transcribe the visible text".',
70
- },
71
- template: {
72
- type: 'string',
73
- enum: TEMPLATE_NAMES,
74
- description:
75
- 'Optional prompt template override. Use screenshot for UI captures, document for PDFs/docs, or default for general images.',
76
- },
77
- },
78
- required: ['filePath'],
79
- additionalProperties: false,
80
- },
81
- },
82
- ];
83
- }
84
-
85
- function writeMessage(message) {
86
- process.stdout.write(`${JSON.stringify(message)}\n`);
87
- }
88
-
89
- function writeResponse(id, result) {
90
- writeMessage({
91
- jsonrpc: '2.0',
92
- id,
93
- result,
94
- });
95
- }
96
-
97
- function writeError(id, code, message) {
98
- writeMessage({
99
- jsonrpc: '2.0',
100
- id,
101
- error: {
102
- code,
103
- message,
104
- },
105
- });
106
- }
107
-
108
- function formatResult(filePath, result, focus) {
109
- const lines = [
110
- '[Image Analysis via MCC]',
111
- '',
112
- `File: ${path.basename(filePath)} (${(result.fileSize / 1024).toFixed(1)} KB)`,
113
- `Model: ${result.model}`,
114
- `Template: ${result.template}`,
115
- ];
116
-
117
- if (focus && focus.trim()) {
118
- lines.push(`Focus: ${focus.trim()}`);
119
- }
120
-
121
- lines.push('', '---', '', result.description);
122
- return lines.join('\n');
123
- }
124
-
125
- function normalizeTemplate(value) {
126
- if (typeof value !== 'string') {
127
- return undefined;
128
- }
129
-
130
- const normalized = value.trim().toLowerCase();
131
- return TEMPLATE_NAMES.includes(normalized) ? normalized : undefined;
132
- }
133
-
134
- function normalizePathForComparison(value) {
135
- return process.platform === 'win32' ? value.toLowerCase() : value;
136
- }
137
-
138
- function isPathWithinWorkspace(workspaceRoot, candidatePath) {
139
- const relativePath = path.relative(workspaceRoot, candidatePath);
140
- return (
141
- relativePath === '' ||
142
- (!relativePath.startsWith('..') &&
143
- !relativePath.startsWith(`..${path.sep}`) &&
144
- !path.isAbsolute(relativePath))
145
- );
146
- }
147
-
148
- function resolveFilePath(toolArgs) {
149
- if (!toolArgs || typeof toolArgs !== 'object') {
150
- return '';
151
- }
152
-
153
- const candidates = [toolArgs.filePath, toolArgs.file_path, toolArgs.path];
154
- for (const candidate of candidates) {
155
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
156
- return candidate.trim();
157
- }
158
- }
159
-
160
- return '';
161
- }
162
-
163
- function resolveWorkspaceFilePath(toolArgs) {
164
- const requestedPath = resolveFilePath(toolArgs);
165
- if (!requestedPath) {
166
- return { filePath: '', error: null };
167
- }
168
-
169
- const workspaceRoot = (() => {
170
- try {
171
- return fs.realpathSync(process.cwd());
172
- } catch {
173
- return path.resolve(process.cwd());
174
- }
175
- })();
176
- const absolutePath = path.resolve(process.cwd(), requestedPath);
177
- const comparisonPath = (() => {
178
- if (fs.existsSync(absolutePath)) {
179
- return fs.realpathSync(absolutePath);
180
- }
181
-
182
- const suffixSegments = [];
183
- let currentPath = absolutePath;
184
- while (!fs.existsSync(currentPath)) {
185
- const parentPath = path.dirname(currentPath);
186
- if (parentPath === currentPath) {
187
- break;
188
- }
189
- suffixSegments.unshift(path.basename(currentPath));
190
- currentPath = parentPath;
191
- }
192
-
193
- const resolvedExistingPath = fs.existsSync(currentPath)
194
- ? fs.realpathSync(currentPath)
195
- : path.resolve(currentPath);
196
- return path.join(resolvedExistingPath, ...suffixSegments);
197
- })();
198
-
199
- if (
200
- !isPathWithinWorkspace(
201
- normalizePathForComparison(workspaceRoot),
202
- normalizePathForComparison(comparisonPath)
203
- )
204
- ) {
205
- return {
206
- filePath: '',
207
- error: 'ImageAnalysis only allows files inside the current workspace.',
208
- };
209
- }
210
-
211
- return {
212
- filePath: absolutePath,
213
- error: null,
214
- };
215
- }
216
-
217
- function resolveFocus(toolArgs) {
218
- return typeof toolArgs.focus === 'string' && toolArgs.focus.trim().length > 0
219
- ? toolArgs.focus.trim()
220
- : undefined;
221
- }
222
-
223
- function formatErrorDetail(filePath, error) {
224
- const message = error instanceof Error ? error.message : String(error);
225
-
226
- if (message.startsWith('FILE_TOO_LARGE:')) {
227
- const fileSizeBytes = Number.parseInt(message.split(':')[1], 10);
228
- const sizeMb = Number.isFinite(fileSizeBytes) ? (fileSizeBytes / 1024 / 1024).toFixed(1) : '?';
229
- return `ImageAnalysis cannot process ${path.basename(filePath)} because it is too large (${sizeMb} MB). The limit is 10 MB.`;
230
- }
231
-
232
- if (message.startsWith('AUTH_ERROR:')) {
233
- return `ImageAnalysis failed because MCC vision auth for this provider is unavailable (${message.split(':')[1]}).`;
234
- }
235
-
236
- if (message.startsWith('RATE_LIMIT:')) {
237
- return `ImageAnalysis hit a provider rate limit while analyzing ${path.basename(filePath)}.`;
238
- }
239
-
240
- if (message.startsWith('API_ERROR:')) {
241
- return `ImageAnalysis failed at the MCC provider route while analyzing ${path.basename(filePath)}.`;
242
- }
243
-
244
- if (
245
- message === 'TIMEOUT' ||
246
- message.includes('timed out') ||
247
- message.includes('timeout') ||
248
- message.includes('ECONNREFUSED') ||
249
- message.includes('ENOTFOUND') ||
250
- message.includes('ENETUNREACH') ||
251
- message.includes('EAI_AGAIN')
252
- ) {
253
- return `ImageAnalysis could not reach the configured MCC provider route for ${path.basename(filePath)}.`;
254
- }
255
-
256
- if (message.includes('EACCES') || message.includes('EPERM')) {
257
- return `ImageAnalysis could not read ${filePath} because access was denied.`;
258
- }
259
-
260
- return `ImageAnalysis failed for ${path.basename(filePath)}: ${message}`;
261
- }
262
-
263
- async function handleToolCall(message) {
264
- const id = message.id;
265
- const params = message.params || {};
266
- const toolArgs = params.arguments || {};
267
- const toolName = params.name || '<missing>';
268
-
269
- log.debug('ToolCall', `=== Tool Call === tool=${toolName} args=${JSON.stringify(toolArgs)} model=${process.env.MCC_IMAGE_ANALYSIS_MODEL || '(not set)'} format=${process.env.MCC_IMAGE_ANALYSIS_FORMAT || '(not set)'} baseUrl=${process.env.MCC_IMAGE_ANALYSIS_RUNTIME_BASE_URL || '(not set)'}`);
270
-
271
- if (!isSupportedToolName(toolName)) {
272
- writeError(id, -32602, `Unknown tool: ${toolName}`);
273
- return;
274
- }
275
-
276
- if (!shouldExposeTools()) {
277
- writeResponse(id, {
278
- content: [
279
- {
280
- type: 'text',
281
- text: 'MCC ImageAnalysis is unavailable for this profile or no provider-backed vision route is ready.',
282
- },
283
- ],
284
- isError: true,
285
- });
286
- return;
287
- }
288
-
289
- const { filePath, error: filePathError } = resolveWorkspaceFilePath(toolArgs);
290
- if (!filePath) {
291
- writeError(
292
- id,
293
- -32602,
294
- filePathError || `Tool "${TOOL_NAME}" requires a non-empty filePath.`
295
- );
296
- return;
297
- }
298
-
299
- if (!fs.existsSync(filePath)) {
300
- writeResponse(id, {
301
- content: [{ type: 'text', text: `ImageAnalysis could not find file: ${filePath}` }],
302
- isError: true,
303
- });
304
- return;
305
- }
306
-
307
- if (!isAnalyzableFile(filePath)) {
308
- writeResponse(id, {
309
- content: [
310
- {
311
- type: 'text',
312
- text: `ImageAnalysis only supports image and PDF files. Use Read for ${path.basename(filePath)} instead.`,
313
- },
314
- ],
315
- isError: true,
316
- });
317
- return;
318
- }
319
-
320
- const focus = resolveFocus(toolArgs);
321
- const template = normalizeTemplate(toolArgs.template);
322
-
323
- log.debug('Resolve', `filePath=${filePath} focus=${focus || '(none)'} template=${template || '(auto)'}`);
324
-
325
- try {
326
- const result = await analyzeFile(filePath, { focus, template });
327
- const responseText = formatResult(filePath, result, focus);
328
- const truncated = responseText.length > 500 ? responseText.substring(0, 500) + '...[truncated]' : responseText;
329
- log.debug('Analyze', `success model=${result.model} descLength=${result.description?.length}`);
330
- log.debug('AnalyzeResponse', truncated.replace(/\n/g, '\\n'));
331
- writeResponse(id, {
332
- content: [{ type: 'text', text: responseText }],
333
- });
334
- } catch (error) {
335
- const errMsg = error instanceof Error ? error.message : String(error);
336
- const errorText = formatErrorDetail(filePath, error);
337
- log.debug('Analyze', `FAILED error=${errMsg} errorText=${errorText}`);
338
- writeResponse(id, {
339
- content: [{ type: 'text', text: errorText }],
340
- isError: true,
341
- });
342
- }
343
- }
344
-
345
- async function handleMessage(message) {
346
- if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
347
- return;
348
- }
349
-
350
- switch (message.method) {
351
- case 'initialize':
352
- writeResponse(message.id, {
353
- protocolVersion: PROTOCOL_VERSION,
354
- capabilities: {
355
- tools: {},
356
- },
357
- serverInfo: {
358
- name: SERVER_NAME,
359
- version: SERVER_VERSION,
360
- },
361
- });
362
- return;
363
- case 'notifications/initialized':
364
- return;
365
- case 'ping':
366
- writeResponse(message.id, {});
367
- return;
368
- case 'tools/list':
369
- writeResponse(message.id, { tools: getTools() });
370
- return;
371
- case 'tools/call':
372
- await handleToolCall(message);
373
- return;
374
- default:
375
- if (message.id !== undefined) {
376
- writeError(message.id, -32601, `Method not found: ${message.method}`);
377
- }
378
- }
379
- }
380
-
381
- let inputBuffer = Buffer.alloc(0);
382
-
383
- function processIncomingBuffer() {
384
- while (true) {
385
- let body;
386
- const startsWithLegacyHeaders = inputBuffer
387
- .slice(0, Math.min(inputBuffer.length, 32))
388
- .toString('utf8')
389
- .toLowerCase()
390
- .startsWith('content-length:');
391
-
392
- if (startsWithLegacyHeaders) {
393
- const headerEnd = inputBuffer.indexOf('\r\n\r\n');
394
- if (headerEnd === -1) {
395
- return;
396
- }
397
-
398
- const headerText = inputBuffer.slice(0, headerEnd).toString('utf8');
399
- const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
400
- if (!contentLengthMatch) {
401
- inputBuffer = Buffer.alloc(0);
402
- return;
403
- }
404
-
405
- const contentLength = Number.parseInt(contentLengthMatch[1], 10);
406
- const messageEnd = headerEnd + 4 + contentLength;
407
- if (inputBuffer.length < messageEnd) {
408
- return;
409
- }
410
-
411
- body = inputBuffer.slice(headerEnd + 4, messageEnd).toString('utf8');
412
- inputBuffer = inputBuffer.slice(messageEnd);
413
- } else {
414
- const newlineIndex = inputBuffer.indexOf('\n');
415
- if (newlineIndex === -1) {
416
- return;
417
- }
418
-
419
- body = inputBuffer.slice(0, newlineIndex).toString('utf8').replace(/\r$/, '').trim();
420
- inputBuffer = inputBuffer.slice(newlineIndex + 1);
421
- if (!body) {
422
- continue;
423
- }
424
- }
425
-
426
- try {
427
- const message = JSON.parse(body);
428
- Promise.resolve(handleMessage(message)).catch((error) => {
429
- if (message && message.id !== undefined) {
430
- writeError(message.id, -32603, (error && error.message) || 'Internal error');
431
- } else if (process.env.MCC_DEBUG) {
432
- console.error(`[mcc-image-analysis] ${error instanceof Error ? error.stack : error}`);
433
- }
434
- });
435
- } catch (error) {
436
- if (process.env.MCC_DEBUG) {
437
- console.error(
438
- `[mcc-image-analysis] Failed to parse JSON-RPC message: ${
439
- error instanceof Error ? error.message : error
440
- }`
441
- );
442
- }
443
- }
444
- }
445
- }
446
-
447
- process.stdin.on('data', (chunk) => {
448
- inputBuffer = Buffer.concat([inputBuffer, chunk]);
449
- processIncomingBuffer();
450
- });
451
-
452
- process.stdin.on('end', () => {
453
- process.exit(0);
454
- });
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function loadRuntimeModule() {
7
+ const candidates = [
8
+ path.join(__dirname, 'image-analysis-runtime.cjs'),
9
+ path.join(__dirname, '../mcp-hooks/image-analysis-runtime.cjs'),
10
+ ];
11
+
12
+ for (const candidate of candidates) {
13
+ if (fs.existsSync(candidate)) {
14
+ return require(candidate);
15
+ }
16
+ }
17
+
18
+ throw new Error(
19
+ `mcc-image-analysis runtime not found. Checked: ${candidates.map((candidate) => path.basename(candidate)).join(', ')}`
20
+ );
21
+ }
22
+
23
+ const { analyzeFile, isAnalyzableFile } = loadRuntimeModule();
24
+
25
+ const PROTOCOL_VERSION = '2024-11-05';
26
+ const SERVER_NAME = 'mcc-image-analysis';
27
+ const SERVER_VERSION = '1.0.0';
28
+ const TOOL_NAME = 'ImageAnalysis';
29
+ const TOOL_ALIASES = ['AnalyzeImage', 'ReadImage'];
30
+ const TEMPLATE_NAMES = ['default', 'screenshot', 'document'];
31
+ const TOOL_DESCRIPTION =
32
+ 'Analyze a local image or PDF file with MCC provider-backed vision. Prefer this tool over Read for image and PDF paths. Use Read for text, code, and other plain files.';
33
+
34
+ // Shared logger — set MCC_LOG_LEVEL=debug to enable
35
+ const log = require('../shared/logger.cjs');
36
+
37
+ function isSupportedToolName(name) {
38
+ return name === TOOL_NAME || TOOL_ALIASES.includes(name);
39
+ }
40
+
41
+ function shouldExposeTools() {
42
+ return (
43
+ process.env.MCC_IMAGE_ANALYSIS_ENABLED === '1' &&
44
+ process.env.MCC_IMAGE_ANALYSIS_SKIP !== '1' &&
45
+ Boolean(process.env.MCC_CURRENT_PROVIDER || process.env.MCC_IMAGE_ANALYSIS_MODEL)
46
+ );
47
+ }
48
+
49
+ function getTools() {
50
+ if (!shouldExposeTools()) {
51
+ return [];
52
+ }
53
+
54
+ return [
55
+ {
56
+ name: TOOL_NAME,
57
+ description: TOOL_DESCRIPTION,
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ filePath: {
62
+ type: 'string',
63
+ description:
64
+ 'Workspace-relative path, or an absolute path inside the current workspace, to a local image or PDF file to analyze.',
65
+ },
66
+ focus: {
67
+ type: 'string',
68
+ description:
69
+ 'Optional question or area of focus, for example "explain the error dialog" or "transcribe the visible text".',
70
+ },
71
+ template: {
72
+ type: 'string',
73
+ enum: TEMPLATE_NAMES,
74
+ description:
75
+ 'Optional prompt template override. Use screenshot for UI captures, document for PDFs/docs, or default for general images.',
76
+ },
77
+ },
78
+ required: ['filePath'],
79
+ additionalProperties: false,
80
+ },
81
+ },
82
+ ];
83
+ }
84
+
85
+ function writeMessage(message) {
86
+ process.stdout.write(`${JSON.stringify(message)}\n`);
87
+ }
88
+
89
+ function writeResponse(id, result) {
90
+ writeMessage({
91
+ jsonrpc: '2.0',
92
+ id,
93
+ result,
94
+ });
95
+ }
96
+
97
+ function writeError(id, code, message) {
98
+ writeMessage({
99
+ jsonrpc: '2.0',
100
+ id,
101
+ error: {
102
+ code,
103
+ message,
104
+ },
105
+ });
106
+ }
107
+
108
+ function formatResult(filePath, result, focus) {
109
+ const lines = [
110
+ '[Image Analysis via MCC]',
111
+ '',
112
+ `File: ${path.basename(filePath)} (${(result.fileSize / 1024).toFixed(1)} KB)`,
113
+ `Model: ${result.model}`,
114
+ `Template: ${result.template}`,
115
+ ];
116
+
117
+ if (focus && focus.trim()) {
118
+ lines.push(`Focus: ${focus.trim()}`);
119
+ }
120
+
121
+ lines.push('', '---', '', result.description);
122
+ return lines.join('\n');
123
+ }
124
+
125
+ function normalizeTemplate(value) {
126
+ if (typeof value !== 'string') {
127
+ return undefined;
128
+ }
129
+
130
+ const normalized = value.trim().toLowerCase();
131
+ return TEMPLATE_NAMES.includes(normalized) ? normalized : undefined;
132
+ }
133
+
134
+ function normalizePathForComparison(value) {
135
+ return process.platform === 'win32' ? value.toLowerCase() : value;
136
+ }
137
+
138
+ function isPathWithinWorkspace(workspaceRoot, candidatePath) {
139
+ const relativePath = path.relative(workspaceRoot, candidatePath);
140
+ return (
141
+ relativePath === '' ||
142
+ (!relativePath.startsWith('..') &&
143
+ !relativePath.startsWith(`..${path.sep}`) &&
144
+ !path.isAbsolute(relativePath))
145
+ );
146
+ }
147
+
148
+ function resolveFilePath(toolArgs) {
149
+ if (!toolArgs || typeof toolArgs !== 'object') {
150
+ return '';
151
+ }
152
+
153
+ const candidates = [toolArgs.filePath, toolArgs.file_path, toolArgs.path];
154
+ for (const candidate of candidates) {
155
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
156
+ return candidate.trim();
157
+ }
158
+ }
159
+
160
+ return '';
161
+ }
162
+
163
+ function resolveWorkspaceFilePath(toolArgs) {
164
+ const requestedPath = resolveFilePath(toolArgs);
165
+ if (!requestedPath) {
166
+ return { filePath: '', error: null };
167
+ }
168
+
169
+ const workspaceRoot = (() => {
170
+ try {
171
+ return fs.realpathSync(process.cwd());
172
+ } catch {
173
+ return path.resolve(process.cwd());
174
+ }
175
+ })();
176
+ const absolutePath = path.resolve(process.cwd(), requestedPath);
177
+ const comparisonPath = (() => {
178
+ if (fs.existsSync(absolutePath)) {
179
+ return fs.realpathSync(absolutePath);
180
+ }
181
+
182
+ const suffixSegments = [];
183
+ let currentPath = absolutePath;
184
+ while (!fs.existsSync(currentPath)) {
185
+ const parentPath = path.dirname(currentPath);
186
+ if (parentPath === currentPath) {
187
+ break;
188
+ }
189
+ suffixSegments.unshift(path.basename(currentPath));
190
+ currentPath = parentPath;
191
+ }
192
+
193
+ const resolvedExistingPath = fs.existsSync(currentPath)
194
+ ? fs.realpathSync(currentPath)
195
+ : path.resolve(currentPath);
196
+ return path.join(resolvedExistingPath, ...suffixSegments);
197
+ })();
198
+
199
+ if (
200
+ !isPathWithinWorkspace(
201
+ normalizePathForComparison(workspaceRoot),
202
+ normalizePathForComparison(comparisonPath)
203
+ )
204
+ ) {
205
+ return {
206
+ filePath: '',
207
+ error: 'ImageAnalysis only allows files inside the current workspace.',
208
+ };
209
+ }
210
+
211
+ return {
212
+ filePath: absolutePath,
213
+ error: null,
214
+ };
215
+ }
216
+
217
+ function resolveFocus(toolArgs) {
218
+ return typeof toolArgs.focus === 'string' && toolArgs.focus.trim().length > 0
219
+ ? toolArgs.focus.trim()
220
+ : undefined;
221
+ }
222
+
223
+ function formatErrorDetail(filePath, error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+
226
+ if (message.startsWith('FILE_TOO_LARGE:')) {
227
+ const fileSizeBytes = Number.parseInt(message.split(':')[1], 10);
228
+ const sizeMb = Number.isFinite(fileSizeBytes) ? (fileSizeBytes / 1024 / 1024).toFixed(1) : '?';
229
+ return `ImageAnalysis cannot process ${path.basename(filePath)} because it is too large (${sizeMb} MB). The limit is 10 MB.`;
230
+ }
231
+
232
+ if (message.startsWith('AUTH_ERROR:')) {
233
+ return `ImageAnalysis failed because MCC vision auth for this provider is unavailable (${message.split(':')[1]}).`;
234
+ }
235
+
236
+ if (message.startsWith('RATE_LIMIT:')) {
237
+ return `ImageAnalysis hit a provider rate limit while analyzing ${path.basename(filePath)}.`;
238
+ }
239
+
240
+ if (message.startsWith('API_ERROR:')) {
241
+ return `ImageAnalysis failed at the MCC provider route while analyzing ${path.basename(filePath)}.`;
242
+ }
243
+
244
+ if (
245
+ message === 'TIMEOUT' ||
246
+ message.includes('timed out') ||
247
+ message.includes('timeout') ||
248
+ message.includes('ECONNREFUSED') ||
249
+ message.includes('ENOTFOUND') ||
250
+ message.includes('ENETUNREACH') ||
251
+ message.includes('EAI_AGAIN')
252
+ ) {
253
+ return `ImageAnalysis could not reach the configured MCC provider route for ${path.basename(filePath)}.`;
254
+ }
255
+
256
+ if (message.includes('EACCES') || message.includes('EPERM')) {
257
+ return `ImageAnalysis could not read ${filePath} because access was denied.`;
258
+ }
259
+
260
+ return `ImageAnalysis failed for ${path.basename(filePath)}: ${message}`;
261
+ }
262
+
263
+ async function handleToolCall(message) {
264
+ const id = message.id;
265
+ const params = message.params || {};
266
+ const toolArgs = params.arguments || {};
267
+ const toolName = params.name || '<missing>';
268
+
269
+ log.debug('ToolCall', `=== Tool Call === tool=${toolName} args=${JSON.stringify(toolArgs)} model=${process.env.MCC_IMAGE_ANALYSIS_MODEL || '(not set)'} format=${process.env.MCC_IMAGE_ANALYSIS_FORMAT || '(not set)'} baseUrl=${process.env.MCC_IMAGE_ANALYSIS_RUNTIME_BASE_URL || '(not set)'}`);
270
+
271
+ if (!isSupportedToolName(toolName)) {
272
+ writeError(id, -32602, `Unknown tool: ${toolName}`);
273
+ return;
274
+ }
275
+
276
+ if (!shouldExposeTools()) {
277
+ writeResponse(id, {
278
+ content: [
279
+ {
280
+ type: 'text',
281
+ text: 'MCC ImageAnalysis is unavailable for this profile or no provider-backed vision route is ready.',
282
+ },
283
+ ],
284
+ isError: true,
285
+ });
286
+ return;
287
+ }
288
+
289
+ const { filePath, error: filePathError } = resolveWorkspaceFilePath(toolArgs);
290
+ if (!filePath) {
291
+ writeError(
292
+ id,
293
+ -32602,
294
+ filePathError || `Tool "${TOOL_NAME}" requires a non-empty filePath.`
295
+ );
296
+ return;
297
+ }
298
+
299
+ if (!fs.existsSync(filePath)) {
300
+ writeResponse(id, {
301
+ content: [{ type: 'text', text: `ImageAnalysis could not find file: ${filePath}` }],
302
+ isError: true,
303
+ });
304
+ return;
305
+ }
306
+
307
+ if (!isAnalyzableFile(filePath)) {
308
+ writeResponse(id, {
309
+ content: [
310
+ {
311
+ type: 'text',
312
+ text: `ImageAnalysis only supports image and PDF files. Use Read for ${path.basename(filePath)} instead.`,
313
+ },
314
+ ],
315
+ isError: true,
316
+ });
317
+ return;
318
+ }
319
+
320
+ const focus = resolveFocus(toolArgs);
321
+ const template = normalizeTemplate(toolArgs.template);
322
+
323
+ log.debug('Resolve', `filePath=${filePath} focus=${focus || '(none)'} template=${template || '(auto)'}`);
324
+
325
+ try {
326
+ const result = await analyzeFile(filePath, { focus, template });
327
+ const responseText = formatResult(filePath, result, focus);
328
+ const truncated = responseText.length > 500 ? responseText.substring(0, 500) + '...[truncated]' : responseText;
329
+ log.debug('Analyze', `success model=${result.model} descLength=${result.description?.length}`);
330
+ log.debug('AnalyzeResponse', truncated.replace(/\n/g, '\\n'));
331
+ writeResponse(id, {
332
+ content: [{ type: 'text', text: responseText }],
333
+ });
334
+ } catch (error) {
335
+ const errMsg = error instanceof Error ? error.message : String(error);
336
+ const errorText = formatErrorDetail(filePath, error);
337
+ log.debug('Analyze', `FAILED error=${errMsg} errorText=${errorText}`);
338
+ writeResponse(id, {
339
+ content: [{ type: 'text', text: errorText }],
340
+ isError: true,
341
+ });
342
+ }
343
+ }
344
+
345
+ async function handleMessage(message) {
346
+ if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
347
+ return;
348
+ }
349
+
350
+ switch (message.method) {
351
+ case 'initialize':
352
+ writeResponse(message.id, {
353
+ protocolVersion: PROTOCOL_VERSION,
354
+ capabilities: {
355
+ tools: {},
356
+ },
357
+ serverInfo: {
358
+ name: SERVER_NAME,
359
+ version: SERVER_VERSION,
360
+ },
361
+ });
362
+ return;
363
+ case 'notifications/initialized':
364
+ return;
365
+ case 'ping':
366
+ writeResponse(message.id, {});
367
+ return;
368
+ case 'tools/list':
369
+ writeResponse(message.id, { tools: getTools() });
370
+ return;
371
+ case 'tools/call':
372
+ await handleToolCall(message);
373
+ return;
374
+ default:
375
+ if (message.id !== undefined) {
376
+ writeError(message.id, -32601, `Method not found: ${message.method}`);
377
+ }
378
+ }
379
+ }
380
+
381
+ let inputBuffer = Buffer.alloc(0);
382
+
383
+ function processIncomingBuffer() {
384
+ while (true) {
385
+ let body;
386
+ const startsWithLegacyHeaders = inputBuffer
387
+ .slice(0, Math.min(inputBuffer.length, 32))
388
+ .toString('utf8')
389
+ .toLowerCase()
390
+ .startsWith('content-length:');
391
+
392
+ if (startsWithLegacyHeaders) {
393
+ const headerEnd = inputBuffer.indexOf('\r\n\r\n');
394
+ if (headerEnd === -1) {
395
+ return;
396
+ }
397
+
398
+ const headerText = inputBuffer.slice(0, headerEnd).toString('utf8');
399
+ const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
400
+ if (!contentLengthMatch) {
401
+ inputBuffer = Buffer.alloc(0);
402
+ return;
403
+ }
404
+
405
+ const contentLength = Number.parseInt(contentLengthMatch[1], 10);
406
+ const messageEnd = headerEnd + 4 + contentLength;
407
+ if (inputBuffer.length < messageEnd) {
408
+ return;
409
+ }
410
+
411
+ body = inputBuffer.slice(headerEnd + 4, messageEnd).toString('utf8');
412
+ inputBuffer = inputBuffer.slice(messageEnd);
413
+ } else {
414
+ const newlineIndex = inputBuffer.indexOf('\n');
415
+ if (newlineIndex === -1) {
416
+ return;
417
+ }
418
+
419
+ body = inputBuffer.slice(0, newlineIndex).toString('utf8').replace(/\r$/, '').trim();
420
+ inputBuffer = inputBuffer.slice(newlineIndex + 1);
421
+ if (!body) {
422
+ continue;
423
+ }
424
+ }
425
+
426
+ try {
427
+ const message = JSON.parse(body);
428
+ Promise.resolve(handleMessage(message)).catch((error) => {
429
+ if (message && message.id !== undefined) {
430
+ writeError(message.id, -32603, (error && error.message) || 'Internal error');
431
+ } else if (process.env.MCC_DEBUG) {
432
+ console.error(`[mcc-image-analysis] ${error instanceof Error ? error.stack : error}`);
433
+ }
434
+ });
435
+ } catch (error) {
436
+ if (process.env.MCC_DEBUG) {
437
+ console.error(
438
+ `[mcc-image-analysis] Failed to parse JSON-RPC message: ${
439
+ error instanceof Error ? error.message : error
440
+ }`
441
+ );
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ process.stdin.on('data', (chunk) => {
448
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
449
+ processIncomingBuffer();
450
+ });
451
+
452
+ process.stdin.on('end', () => {
453
+ process.exit(0);
454
+ });