ticketlens 0.38.19 → 0.38.21
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/package.json +1 -1
- package/skills/jtb/scripts/lib/doctor-checks.mjs +85 -1
- package/skills/jtb/scripts/lib/doctor-command.mjs +29 -6
- package/skills/jtb/scripts/lib/help.mjs +6 -3
- package/skills/jtb/scripts/lib/mcp-handshake-checker.mjs +82 -0
- package/skills/jtb/scripts/lib/mcp-install.mjs +5 -4
- package/skills/jtb/scripts/lib/mcp-server.mjs +1 -1
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* before any public (CLI plain/JSON or MCP) output.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
15
17
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
16
18
|
import { resolveProfile, loadCredentials, loadProfiles } from './profile-resolver.mjs';
|
|
17
19
|
import { checkLicense, GRACE_DAYS } from './license.mjs';
|
|
@@ -21,6 +23,8 @@ import { testConnections } from './connection-tester.mjs';
|
|
|
21
23
|
import { formatSize } from './attachment-downloader.mjs';
|
|
22
24
|
import { getCacheEntries, filterEntriesByProfile } from './cache-manager.mjs';
|
|
23
25
|
import { readQueue } from './recall-queue.mjs';
|
|
26
|
+
import { readMcpConfig, ENTRY_NAME, DESIRED_MCP_ENTRY } from './mcp-install.mjs';
|
|
27
|
+
import { testMcpHandshake, DEFAULT_HANDSHAKE_TIMEOUT_MS } from './mcp-handshake-checker.mjs';
|
|
24
28
|
|
|
25
29
|
const NOOP_STREAM = { write: () => true };
|
|
26
30
|
|
|
@@ -155,7 +159,7 @@ export async function checkConnectivity({
|
|
|
155
159
|
}
|
|
156
160
|
const summary = results
|
|
157
161
|
.map(r => r.ok ? `${r.name}: ok` : `${r.name}: ${r.error}${r.hint ? ` → ${r.hint}` : ''}`)
|
|
158
|
-
.join('
|
|
162
|
+
.join('\n');
|
|
159
163
|
return {
|
|
160
164
|
id: 'connectivity', label: 'Tracker connectivity',
|
|
161
165
|
ok: failedCount === 0,
|
|
@@ -217,3 +221,83 @@ export function checkRecallQueue({
|
|
|
217
221
|
fixable: true,
|
|
218
222
|
};
|
|
219
223
|
}
|
|
224
|
+
|
|
225
|
+
export function checkMcpRegistration({
|
|
226
|
+
cwd = process.cwd(),
|
|
227
|
+
existsSyncFn = existsSync,
|
|
228
|
+
readMcpConfigFn = readMcpConfig,
|
|
229
|
+
} = {}) {
|
|
230
|
+
const configPath = join(cwd, '.mcp.json');
|
|
231
|
+
const read = readMcpConfigFn(configPath);
|
|
232
|
+
|
|
233
|
+
if (!read.ok) {
|
|
234
|
+
return {
|
|
235
|
+
id: 'mcp-registration', label: 'MCP registration', ok: false,
|
|
236
|
+
message: read.reason, hint: null, fixable: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const existing = read.config.mcpServers?.[ENTRY_NAME];
|
|
241
|
+
const registered = existing !== undefined && JSON.stringify(existing) === JSON.stringify(DESIRED_MCP_ENTRY);
|
|
242
|
+
|
|
243
|
+
if (registered) {
|
|
244
|
+
return {
|
|
245
|
+
id: 'mcp-registration', label: 'MCP registration', ok: true,
|
|
246
|
+
message: '"ticketlens" is registered in .mcp.json with the correct command and args.',
|
|
247
|
+
hint: null, fixable: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!existsSyncFn(configPath)) {
|
|
252
|
+
return {
|
|
253
|
+
id: 'mcp-registration', label: 'MCP registration', ok: false,
|
|
254
|
+
message: 'No .mcp.json file found in the current directory.',
|
|
255
|
+
hint: 'Run `ticketlens mcp install` to create it and register "ticketlens".',
|
|
256
|
+
fixable: true,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
id: 'mcp-registration', label: 'MCP registration', ok: false,
|
|
262
|
+
message: '"ticketlens" is not registered in .mcp.json.',
|
|
263
|
+
hint: 'Run `ticketlens mcp install` to register it.',
|
|
264
|
+
fixable: true,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 'timeout' is handled separately below (its message embeds the actual
|
|
269
|
+
// timeoutMs), so it has no entry here.
|
|
270
|
+
const MCP_HANDSHAKE_MESSAGES = {
|
|
271
|
+
'spawn-error': 'Could not start "ticketlens mcp" — command not found or failed to launch.',
|
|
272
|
+
'invalid-response': '"ticketlens mcp" responded, but not with a valid initialize result.',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const MCP_HANDSHAKE_HINTS = {
|
|
276
|
+
'spawn-error': 'Confirm `ticketlens` is installed and on PATH, then run `ticketlens doctor --mcp` again.',
|
|
277
|
+
'timeout': 'Run `ticketlens doctor --mcp` again — if it keeps timing out, check for a hung `ticketlens mcp` process.',
|
|
278
|
+
'invalid-response': 'Run `ticketlens --version` to check the installed build; reinstall if the response looks corrupted.',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export async function checkMcpHandshake({
|
|
282
|
+
timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
283
|
+
testMcpHandshakeFn = testMcpHandshake,
|
|
284
|
+
} = {}) {
|
|
285
|
+
const result = await testMcpHandshakeFn({ timeoutMs });
|
|
286
|
+
|
|
287
|
+
if (result.ok) {
|
|
288
|
+
return {
|
|
289
|
+
id: 'mcp-handshake', label: 'MCP server handshake', ok: true,
|
|
290
|
+
message: `MCP server handshake succeeded, protocol version ${result.protocolVersion}.`,
|
|
291
|
+
hint: null, fixable: false,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const message = result.reason === 'timeout'
|
|
296
|
+
? `No response from "ticketlens mcp" within ${timeoutMs}ms.`
|
|
297
|
+
: MCP_HANDSHAKE_MESSAGES[result.reason] ?? 'MCP server handshake failed.';
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
id: 'mcp-handshake', label: 'MCP server handshake', ok: false,
|
|
301
|
+
message, hint: MCP_HANDSHAKE_HINTS[result.reason] ?? null, fixable: false,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -10,15 +10,20 @@ import { handleUnknownFlags } from './arg-validator.mjs';
|
|
|
10
10
|
import { createStyler } from './ansi.mjs';
|
|
11
11
|
import {
|
|
12
12
|
checkProfileConfig, checkLicenseFreshness, checkConnectivity,
|
|
13
|
-
checkCacheHealth, checkRecallQueue,
|
|
13
|
+
checkCacheHealth, checkRecallQueue, checkMcpRegistration, checkMcpHandshake,
|
|
14
14
|
} from './doctor-checks.mjs';
|
|
15
15
|
import { revalidateLicense } from './license.mjs';
|
|
16
16
|
import { flushQueue } from './recall-queue.mjs';
|
|
17
17
|
import { readCliToken } from './cli-auth.mjs';
|
|
18
|
+
import { mcpInstall } from './mcp-install.mjs';
|
|
18
19
|
|
|
19
|
-
const KNOWN_FLAGS = ['--format=', '--fix', '--profile=', '--help', '-h'];
|
|
20
|
+
const KNOWN_FLAGS = ['--format=', '--fix', '--profile=', '--mcp', '--help', '-h'];
|
|
21
|
+
const NOOP_STREAM = { write: () => true };
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
// A leading run of 2+ capitals is an acronym (e.g. "MCP registration") and
|
|
24
|
+
// must not be partially lowercased — only a genuine single leading capital
|
|
25
|
+
// (e.g. "Profile configuration") gets folded to start a sentence mid-line.
|
|
26
|
+
const lowerFirst = (str) => (/^[A-Z]{2,}/.test(str) ? str : str.charAt(0).toLowerCase() + str.slice(1));
|
|
22
27
|
|
|
23
28
|
function renderPlain(checks, { fixed, skipped, stream }) {
|
|
24
29
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
@@ -26,7 +31,9 @@ function renderPlain(checks, { fixed, skipped, stream }) {
|
|
|
26
31
|
for (const check of checks) {
|
|
27
32
|
const icon = check.ok ? s.green('✔') : s.red('✖');
|
|
28
33
|
stream.write(` ${icon} ${check.label}: ${check.message}\n`);
|
|
29
|
-
if (!check.ok && check.hint)
|
|
34
|
+
if (!check.ok && check.hint) {
|
|
35
|
+
for (const line of check.hint.split('\n')) stream.write(` ${s.dim(line)}\n`);
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
if (fixed.length > 0) {
|
|
32
39
|
stream.write(`\n ${s.green('Fixed:')} ${fixed.join(', ')}\n`);
|
|
@@ -39,10 +46,11 @@ function renderPlain(checks, { fixed, skipped, stream }) {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
async function applyFixes(rawResults, {
|
|
42
|
-
configDir, profileName, format, stream,
|
|
49
|
+
configDir, profileName, cwd, format, stream,
|
|
43
50
|
revalidateLicenseFn, checkLicenseFreshnessFn,
|
|
44
51
|
unlinkFn, checkCacheHealthFn,
|
|
45
52
|
flushQueueFn, checkRecallQueueFn, readCliTokenFn,
|
|
53
|
+
mcpInstallFn, checkMcpRegistrationFn,
|
|
46
54
|
}) {
|
|
47
55
|
const fixed = [];
|
|
48
56
|
const skipped = [];
|
|
@@ -79,6 +87,14 @@ async function applyFixes(rawResults, {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
if (byId['mcp-registration'] && !byId['mcp-registration'].ok && byId['mcp-registration'].fixable) {
|
|
91
|
+
if (format === 'plain') stream.write('Registering ticketlens as an MCP server...\n');
|
|
92
|
+
mcpInstallFn({ cwd, stream: NOOP_STREAM, dryRun: false });
|
|
93
|
+
const recheck = checkMcpRegistrationFn({ cwd });
|
|
94
|
+
byId['mcp-registration'] = recheck;
|
|
95
|
+
if (recheck.ok) fixed.push('mcp-registration');
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
return { results: Object.values(byId), fixed, skipped };
|
|
83
99
|
}
|
|
84
100
|
|
|
@@ -92,10 +108,13 @@ export async function runDoctor(args, {
|
|
|
92
108
|
checkConnectivityFn = checkConnectivity,
|
|
93
109
|
checkCacheHealthFn = checkCacheHealth,
|
|
94
110
|
checkRecallQueueFn = checkRecallQueue,
|
|
111
|
+
checkMcpRegistrationFn = checkMcpRegistration,
|
|
112
|
+
checkMcpHandshakeFn = checkMcpHandshake,
|
|
95
113
|
revalidateLicenseFn = revalidateLicense,
|
|
96
114
|
unlinkFn = (p) => fs.unlinkSync(p),
|
|
97
115
|
flushQueueFn = flushQueue,
|
|
98
116
|
readCliTokenFn = readCliToken,
|
|
117
|
+
mcpInstallFn = mcpInstall,
|
|
99
118
|
} = {}) {
|
|
100
119
|
const validated = await handleUnknownFlags(args, KNOWN_FLAGS, { stream });
|
|
101
120
|
if (validated === null) return { ok: false };
|
|
@@ -110,6 +129,7 @@ export async function runDoctor(args, {
|
|
|
110
129
|
const profileArg = validated.find(a => a.startsWith('--profile='));
|
|
111
130
|
const profileName = profileArg ? profileArg.split('=')[1] : null;
|
|
112
131
|
const shouldFix = validated.includes('--fix');
|
|
132
|
+
const shouldCheckMcpHandshake = validated.includes('--mcp');
|
|
113
133
|
|
|
114
134
|
const checkList = [
|
|
115
135
|
{ label: 'Profile configuration', run: () => checkProfileConfigFn({ configDir, profileName, cwd }) },
|
|
@@ -117,6 +137,8 @@ export async function runDoctor(args, {
|
|
|
117
137
|
{ label: 'Tracker connectivity', run: () => checkConnectivityFn({ configDir, profileName, cwd }) },
|
|
118
138
|
{ label: 'Attachment cache', run: () => checkCacheHealthFn({ configDir, profileName }) },
|
|
119
139
|
{ label: 'Recall sync queue', run: () => checkRecallQueueFn({ configDir }) },
|
|
140
|
+
{ label: 'MCP registration', run: () => checkMcpRegistrationFn({ cwd }) },
|
|
141
|
+
...(shouldCheckMcpHandshake ? [{ label: 'MCP server handshake', run: () => checkMcpHandshakeFn({}) }] : []),
|
|
120
142
|
];
|
|
121
143
|
|
|
122
144
|
const showProgress = format === 'plain' && stream.isTTY;
|
|
@@ -136,10 +158,11 @@ export async function runDoctor(args, {
|
|
|
136
158
|
let finalResults = rawResults;
|
|
137
159
|
if (shouldFix) {
|
|
138
160
|
const applied = await applyFixes(rawResults, {
|
|
139
|
-
configDir, profileName, format, stream,
|
|
161
|
+
configDir, profileName, cwd, format, stream,
|
|
140
162
|
revalidateLicenseFn, checkLicenseFreshnessFn,
|
|
141
163
|
unlinkFn, checkCacheHealthFn,
|
|
142
164
|
flushQueueFn, checkRecallQueueFn, readCliTokenFn,
|
|
165
|
+
mcpInstallFn, checkMcpRegistrationFn,
|
|
143
166
|
});
|
|
144
167
|
finalResults = applied.results;
|
|
145
168
|
fixed = applied.fixed;
|
|
@@ -1230,11 +1230,12 @@ export function printDoctorHelp({ stream = process.stdout } = {}) {
|
|
|
1230
1230
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
1231
1231
|
const lines = [
|
|
1232
1232
|
'',
|
|
1233
|
-
` ${s.bold(s.brand('ticketlens'))} ${s.bold('doctor')} ${s.dim('[--fix] [--format=plain|json] [--profile=NAME]')}`,
|
|
1233
|
+
` ${s.bold(s.brand('ticketlens'))} ${s.bold('doctor')} ${s.dim('[--fix] [--format=plain|json] [--profile=NAME] [--mcp]')}`,
|
|
1234
1234
|
'',
|
|
1235
1235
|
` Diagnose common TicketLens problems: profile configuration, license`,
|
|
1236
|
-
` freshness, tracker connectivity, attachment cache health,
|
|
1237
|
-
` sync queue.
|
|
1236
|
+
` freshness, tracker connectivity, attachment cache health, MCP`,
|
|
1237
|
+
` registration, and the Recall sync queue. Pass --mcp to also check the`,
|
|
1238
|
+
` MCP server handshake. Free tier, fully unrestricted.`,
|
|
1238
1239
|
'',
|
|
1239
1240
|
` ${s.bold('OPTIONS')}`,
|
|
1240
1241
|
'',
|
|
@@ -1242,6 +1243,7 @@ export function printDoctorHelp({ stream = process.stdout } = {}) {
|
|
|
1242
1243
|
` ${s.brand('--format')}=${s.dim('plain')} Human-readable output ${s.dim('(default)')}`,
|
|
1243
1244
|
` ${s.brand('--format')}=${s.dim('json')} JSON output for scripting/piping`,
|
|
1244
1245
|
` ${s.brand('--profile')}=${s.dim('NAME')} Scope profile/connectivity/cache checks to one profile`,
|
|
1246
|
+
` ${s.brand('--mcp')} Also run the MCP server handshake check (spawns a subprocess)`,
|
|
1245
1247
|
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
1246
1248
|
'',
|
|
1247
1249
|
` ${s.bold('EXAMPLES')}`,
|
|
@@ -1250,6 +1252,7 @@ export function printDoctorHelp({ stream = process.stdout } = {}) {
|
|
|
1250
1252
|
` ${s.dim('$')} ticketlens doctor --fix`,
|
|
1251
1253
|
` ${s.dim('$')} ticketlens doctor --profile=work`,
|
|
1252
1254
|
` ${s.dim('$')} ticketlens doctor --format=json`,
|
|
1255
|
+
` ${s.dim('$')} ticketlens doctor --mcp`,
|
|
1253
1256
|
'',
|
|
1254
1257
|
];
|
|
1255
1258
|
stream.write(lines.join('\n') + '\n');
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawns `ticketlens mcp` as a child process and speaks one round of the
|
|
3
|
+
* MCP stdio JSON-RPC handshake to it — the same `initialize` request an AI
|
|
4
|
+
* harness sends per the MCP stdio transport spec (see mcp-server.mjs's own
|
|
5
|
+
* `initialize` handler). Pure diagnostic probe: the child is always killed
|
|
6
|
+
* before this resolves, on every path (success, timeout, or error) — never
|
|
7
|
+
* left running.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5000;
|
|
13
|
+
const PROTOCOL_VERSION = '2025-11-25';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @returns {Promise<{ok: boolean, reason?: 'spawn-error'|'timeout'|'invalid-response', error?: Error, protocolVersion?: string}>}
|
|
17
|
+
*/
|
|
18
|
+
export function testMcpHandshake({
|
|
19
|
+
spawnFn = spawn,
|
|
20
|
+
timeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
21
|
+
} = {}) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let settled = false;
|
|
24
|
+
let buffer = '';
|
|
25
|
+
let child;
|
|
26
|
+
|
|
27
|
+
const settle = (value) => {
|
|
28
|
+
if (settled) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
try { child?.kill(); } catch { /* already exited */ }
|
|
32
|
+
resolve(value);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const timer = setTimeout(() => settle({ ok: false, reason: 'timeout' }), timeoutMs);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
child = spawnFn('ticketlens', ['mcp'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
settle({ ok: false, reason: 'spawn-error', error });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
child.on('error', (error) => settle({ ok: false, reason: 'spawn-error', error }));
|
|
45
|
+
// A write can fail asynchronously (e.g. EPIPE if the child already died) —
|
|
46
|
+
// an unhandled 'error' on a Writable stream crashes the process. The
|
|
47
|
+
// child's own 'error'/'exit' already attributes the real failure reason;
|
|
48
|
+
// this only needs to stop the crash, not settle a second time.
|
|
49
|
+
child.stdin.on('error', () => {});
|
|
50
|
+
|
|
51
|
+
child.stdout.on('data', (chunk) => {
|
|
52
|
+
buffer += chunk.toString();
|
|
53
|
+
const newlineIdx = buffer.indexOf('\n');
|
|
54
|
+
if (newlineIdx === -1) return;
|
|
55
|
+
const line = buffer.slice(0, newlineIdx);
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(line);
|
|
59
|
+
} catch {
|
|
60
|
+
settle({ ok: false, reason: 'invalid-response' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (msg?.id === 1 && msg.result?.protocolVersion) {
|
|
64
|
+
settle({ ok: true, protocolVersion: msg.result.protocolVersion });
|
|
65
|
+
} else {
|
|
66
|
+
settle({ ok: false, reason: 'invalid-response' });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const request = JSON.stringify({
|
|
71
|
+
jsonrpc: '2.0',
|
|
72
|
+
id: 1,
|
|
73
|
+
method: 'initialize',
|
|
74
|
+
params: {
|
|
75
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
76
|
+
capabilities: {},
|
|
77
|
+
clientInfo: { name: 'ticketlens-doctor', version: '1.0' },
|
|
78
|
+
},
|
|
79
|
+
}) + '\n';
|
|
80
|
+
child.stdin.write(request);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -20,9 +20,10 @@ import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
|
20
20
|
import { isLicensed } from './license.mjs';
|
|
21
21
|
import { createStyler } from './ansi.mjs';
|
|
22
22
|
|
|
23
|
-
const ENTRY_NAME = 'ticketlens';
|
|
23
|
+
export const ENTRY_NAME = 'ticketlens';
|
|
24
|
+
export const DESIRED_MCP_ENTRY = { command: ENTRY_NAME, args: ['mcp'] };
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
+
export function readMcpConfig(configPath) {
|
|
26
27
|
if (!existsSync(configPath)) return { ok: true, config: {} };
|
|
27
28
|
let parsed;
|
|
28
29
|
try {
|
|
@@ -55,7 +56,7 @@ export function mcpInstall({
|
|
|
55
56
|
const configPath = join(cwd, '.mcp.json');
|
|
56
57
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
57
58
|
|
|
58
|
-
const read =
|
|
59
|
+
const read = readMcpConfig(configPath);
|
|
59
60
|
if (!read.ok) {
|
|
60
61
|
stream.write(` ${s.dim('✗')} ${read.reason}\n`);
|
|
61
62
|
return { written: false, dryRun, path: configPath, reason: read.reason };
|
|
@@ -63,7 +64,7 @@ export function mcpInstall({
|
|
|
63
64
|
|
|
64
65
|
const config = read.config;
|
|
65
66
|
config.mcpServers ??= {};
|
|
66
|
-
const desired =
|
|
67
|
+
const desired = DESIRED_MCP_ENTRY;
|
|
67
68
|
const existing = config.mcpServers[ENTRY_NAME];
|
|
68
69
|
const alreadyCorrect = existing !== undefined && JSON.stringify(existing) === JSON.stringify(desired);
|
|
69
70
|
config.mcpServers[ENTRY_NAME] = desired;
|
|
@@ -31,7 +31,7 @@ const PROTOCOL_VERSION = '2025-11-25';
|
|
|
31
31
|
const TOOLS = [
|
|
32
32
|
{
|
|
33
33
|
name: 'doctor',
|
|
34
|
-
description: 'Diagnose common TicketLens problems: profile configuration, license freshness, tracker connectivity, attachment cache health, and the Recall sync queue. Always returns structured JSON. Free tier, fully unrestricted — including fix.',
|
|
34
|
+
description: 'Diagnose common TicketLens problems: profile configuration, license freshness, tracker connectivity, attachment cache health, MCP registration, and the Recall sync queue. Always returns structured JSON. Free tier, fully unrestricted — including fix.',
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
properties: {
|