@salesforce/b2c-dx-mcp 1.1.3 → 1.2.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.
Files changed (50) hide show
  1. package/dist/registry.js +21 -14
  2. package/dist/server-context.d.ts +6 -4
  3. package/dist/server-context.js +8 -5
  4. package/dist/tools/diagnostics/debug-capture-at-breakpoint.js +1 -1
  5. package/dist/tools/diagnostics/debug-continue.js +1 -1
  6. package/dist/tools/diagnostics/debug-end-session.js +1 -1
  7. package/dist/tools/diagnostics/debug-evaluate.js +1 -1
  8. package/dist/tools/diagnostics/debug-get-stack.js +1 -1
  9. package/dist/tools/diagnostics/debug-get-variables.js +1 -1
  10. package/dist/tools/diagnostics/debug-list-sessions.js +1 -1
  11. package/dist/tools/diagnostics/debug-set-breakpoints.js +1 -1
  12. package/dist/tools/diagnostics/debug-start-session.js +1 -1
  13. package/dist/tools/diagnostics/debug-step.js +1 -1
  14. package/dist/tools/diagnostics/debug-wait-for-stop.js +1 -1
  15. package/dist/tools/diagnostics/index.d.ts +6 -1
  16. package/dist/tools/diagnostics/index.js +13 -1
  17. package/dist/tools/diagnostics/log-watch-registry.d.ts +94 -0
  18. package/dist/tools/diagnostics/log-watch-registry.js +249 -0
  19. package/dist/tools/diagnostics/logs-get-recent.d.ts +9 -0
  20. package/dist/tools/diagnostics/logs-get-recent.js +68 -0
  21. package/dist/tools/diagnostics/logs-list-files.d.ts +9 -0
  22. package/dist/tools/diagnostics/logs-list-files.js +36 -0
  23. package/dist/tools/diagnostics/logs-watch-list.d.ts +4 -0
  24. package/dist/tools/diagnostics/logs-watch-list.js +36 -0
  25. package/dist/tools/diagnostics/logs-watch-poll.d.ts +4 -0
  26. package/dist/tools/diagnostics/logs-watch-poll.js +63 -0
  27. package/dist/tools/diagnostics/logs-watch-start.d.ts +9 -0
  28. package/dist/tools/diagnostics/logs-watch-start.js +117 -0
  29. package/dist/tools/diagnostics/logs-watch-stop.d.ts +4 -0
  30. package/dist/tools/diagnostics/logs-watch-stop.js +36 -0
  31. package/dist/tools/docs/docs-list.d.ts +3 -0
  32. package/dist/tools/docs/docs-list.js +22 -0
  33. package/dist/tools/docs/docs-read.d.ts +3 -0
  34. package/dist/tools/docs/docs-read.js +25 -0
  35. package/dist/tools/docs/docs-schema-list.d.ts +3 -0
  36. package/dist/tools/docs/docs-schema-list.js +21 -0
  37. package/dist/tools/docs/docs-schema-read.d.ts +3 -0
  38. package/dist/tools/docs/docs-schema-read.js +25 -0
  39. package/dist/tools/docs/docs-schema-search.d.ts +3 -0
  40. package/dist/tools/docs/docs-schema-search.js +26 -0
  41. package/dist/tools/docs/docs-search.d.ts +3 -0
  42. package/dist/tools/docs/docs-search.js +27 -0
  43. package/dist/tools/docs/index.d.ts +3 -0
  44. package/dist/tools/docs/index.js +22 -0
  45. package/dist/tools/index.d.ts +1 -0
  46. package/dist/tools/index.js +1 -0
  47. package/dist/utils/constants.d.ts +2 -2
  48. package/dist/utils/constants.js +1 -1
  49. package/oclif.manifest.json +4 -2
  50. package/package.json +4 -4
@@ -0,0 +1,249 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { randomUUID } from 'node:crypto';
7
+ import { getLogger } from '@salesforce/b2c-tooling-sdk/logging';
8
+ const IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
9
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
10
+ /** Maximum number of buffered entries before oldest are evicted. */
11
+ export const DEFAULT_BUFFER_CAP = 5000;
12
+ /**
13
+ * Maximum cumulative bytes buffered before oldest entries are evicted.
14
+ * Guards against a small number of pathologically large entries (e.g. multi-MB
15
+ * stack traces) blowing past the count cap's intended memory bound.
16
+ */
17
+ export const DEFAULT_BUFFER_BYTES_CAP = 50 * 1024 * 1024; // 50 MB
18
+ /** Maximum number of buffered errors before oldest are evicted. */
19
+ const ERROR_CAP = 25;
20
+ /** Maximum number of buffered rotation events before oldest are evicted. */
21
+ const ROTATION_CAP = 100;
22
+ /** Approximate in-memory byte cost of a buffered entry. */
23
+ function entryBytes(entry) {
24
+ return (entry.raw?.length ?? 0) + (entry.message?.length ?? 0);
25
+ }
26
+ export class LogWatchRegistry {
27
+ cleanupTimer;
28
+ watches = new Map();
29
+ constructor() {
30
+ this.cleanupTimer = setInterval(() => {
31
+ this.cleanupIdleWatches().catch(() => { });
32
+ }, CLEANUP_INTERVAL_MS);
33
+ this.cleanupTimer.unref();
34
+ }
35
+ /**
36
+ * Append a log entry to a watch buffer. Evicts oldest if over cap.
37
+ * Resolves any pending poll waiters.
38
+ */
39
+ appendEntry(watchId, entry) {
40
+ const w = this.watches.get(watchId);
41
+ if (!w || w.stopped)
42
+ return;
43
+ w.buffer.push(entry);
44
+ w.bufferBytes += entryBytes(entry);
45
+ w.totalEntriesSeen += 1;
46
+ // Evict oldest entries until under BOTH the count cap and the byte cap.
47
+ // The byte cap guards against a few pathologically large entries (e.g.
48
+ // multi-MB stack traces) exceeding the intended memory bound. Always keep
49
+ // at least one entry so a single oversized entry is still deliverable.
50
+ while (w.buffer.length > 1 && (w.buffer.length > w.bufferCap || w.bufferBytes > w.bufferBytesCap)) {
51
+ const dropped = w.buffer.shift();
52
+ w.bufferBytes -= entryBytes(dropped);
53
+ w.droppedEntries += 1;
54
+ }
55
+ w.lastActivityAt = Date.now();
56
+ this.flushWaiters(w);
57
+ }
58
+ appendError(watchId, err) {
59
+ const w = this.watches.get(watchId);
60
+ if (!w || w.stopped)
61
+ return;
62
+ w.errors.push({ message: err.message, at: new Date().toISOString() });
63
+ if (w.errors.length > ERROR_CAP) {
64
+ w.errors.splice(0, w.errors.length - ERROR_CAP);
65
+ }
66
+ w.lastActivityAt = Date.now();
67
+ }
68
+ /**
69
+ * Append a discovered file (deduped by name).
70
+ */
71
+ appendFileDiscovered(watchId, file) {
72
+ const w = this.watches.get(watchId);
73
+ if (!w || w.stopped)
74
+ return;
75
+ if (!w.filesDiscovered.some((f) => f.name === file.name)) {
76
+ w.filesDiscovered.push(file);
77
+ // Queue for the next poll drain so each newly-discovered file is reported
78
+ // exactly once, while filesDiscovered keeps the cumulative view for list.
79
+ w.pendingFilesDiscovered.push(file);
80
+ }
81
+ w.lastActivityAt = Date.now();
82
+ }
83
+ appendRotation(watchId, file) {
84
+ const w = this.watches.get(watchId);
85
+ if (!w || w.stopped)
86
+ return;
87
+ w.rotations.push(file);
88
+ if (w.rotations.length > ROTATION_CAP) {
89
+ w.rotations.splice(0, w.rotations.length - ROTATION_CAP);
90
+ }
91
+ w.lastActivityAt = Date.now();
92
+ this.flushWaiters(w);
93
+ }
94
+ async destroyAll() {
95
+ if (this.cleanupTimer) {
96
+ clearInterval(this.cleanupTimer);
97
+ this.cleanupTimer = undefined;
98
+ }
99
+ const promises = [...this.watches.keys()].map((id) => this.destroyWatch(id));
100
+ await Promise.allSettled(promises);
101
+ }
102
+ async destroyWatch(watchId) {
103
+ const w = this.watches.get(watchId);
104
+ if (!w)
105
+ return;
106
+ w.stopped = true;
107
+ // Resolve any in-flight waiters so callers don't hang
108
+ for (const waiter of w.pollWaiters) {
109
+ clearTimeout(waiter.timer);
110
+ waiter.resolve();
111
+ }
112
+ w.pollWaiters.length = 0;
113
+ try {
114
+ await w.stop();
115
+ }
116
+ catch {
117
+ // Best-effort
118
+ }
119
+ try {
120
+ await w.done;
121
+ }
122
+ catch {
123
+ // Tail may reject if it was in the middle of polling; ignore.
124
+ }
125
+ this.watches.delete(watchId);
126
+ }
127
+ /**
128
+ * Drain buffered entries (up to maxEntries) and any rotation/error events
129
+ * from the watch. Removes drained items from the underlying buffers.
130
+ */
131
+ drain(watchId, maxEntries) {
132
+ const w = this.getWatchOrThrow(watchId);
133
+ const entries = w.buffer.splice(0, maxEntries);
134
+ for (const e of entries) {
135
+ w.bufferBytes -= entryBytes(e);
136
+ }
137
+ if (w.bufferBytes < 0 || w.buffer.length === 0)
138
+ w.bufferBytes = 0;
139
+ const truncated = w.buffer.length > 0;
140
+ const rotations = w.rotations.splice(0);
141
+ const errors = w.errors.splice(0);
142
+ // Only files discovered since the last poll — drained like rotations/errors.
143
+ const filesDiscovered = w.pendingFilesDiscovered.splice(0);
144
+ w.lastActivityAt = Date.now();
145
+ return { entries, errors, filesDiscovered, rotations, truncated };
146
+ }
147
+ findByHostname(hostname) {
148
+ for (const w of this.watches.values()) {
149
+ if (w.hostname === hostname)
150
+ return w;
151
+ }
152
+ return undefined;
153
+ }
154
+ getWatch(watchId) {
155
+ return this.watches.get(watchId);
156
+ }
157
+ getWatchOrThrow(watchId) {
158
+ const w = this.watches.get(watchId);
159
+ if (!w) {
160
+ throw new Error(`No log watch found with id "${watchId}". Use logs_watch_list to see active watches.`);
161
+ }
162
+ w.lastActivityAt = Date.now();
163
+ return w;
164
+ }
165
+ listWatches() {
166
+ return [...this.watches.values()];
167
+ }
168
+ registerWatch(opts) {
169
+ const { hostname, prefixes, tailResult, bufferCap = DEFAULT_BUFFER_CAP, bufferBytesCap = DEFAULT_BUFFER_BYTES_CAP, } = opts;
170
+ const existing = this.findByHostname(hostname);
171
+ if (existing) {
172
+ throw new Error(`A log watch already exists for ${hostname} (watch_id: "${existing.watchId}"). ` +
173
+ `Stop it with logs_watch_stop first, or poll the existing watch.`);
174
+ }
175
+ const watchId = randomUUID();
176
+ const now = Date.now();
177
+ const entry = {
178
+ buffer: [],
179
+ bufferBytes: 0,
180
+ bufferBytesCap,
181
+ bufferCap,
182
+ createdAt: now,
183
+ done: tailResult.done,
184
+ droppedEntries: 0,
185
+ errors: [],
186
+ filesDiscovered: [],
187
+ hostname,
188
+ lastActivityAt: now,
189
+ pendingFilesDiscovered: [],
190
+ pollWaiters: [],
191
+ prefixes,
192
+ rotations: [],
193
+ stop: tailResult.stop,
194
+ stopped: false,
195
+ totalEntriesSeen: 0,
196
+ watchId,
197
+ };
198
+ this.watches.set(watchId, entry);
199
+ return entry;
200
+ }
201
+ /**
202
+ * Wait until at least one entry is buffered (or rotation/error event), or until
203
+ * timeout elapses. Returns immediately if the buffer is non-empty.
204
+ */
205
+ async waitForActivity(watchId, timeoutMs) {
206
+ const w = this.getWatchOrThrow(watchId);
207
+ if (w.buffer.length > 0 || w.rotations.length > 0 || w.errors.length > 0 || w.stopped) {
208
+ return;
209
+ }
210
+ return new Promise((resolve) => {
211
+ const timer = setTimeout(() => {
212
+ const idx = w.pollWaiters.findIndex((waiter) => waiter.timer === timer);
213
+ if (idx !== -1)
214
+ w.pollWaiters.splice(idx, 1);
215
+ resolve();
216
+ }, timeoutMs);
217
+ w.pollWaiters.push({ resolve: () => resolve(), timer });
218
+ });
219
+ }
220
+ async cleanupIdleWatches() {
221
+ const logger = getLogger();
222
+ const now = Date.now();
223
+ const idle = [...this.watches.entries()].filter(([, w]) => now - w.lastActivityAt > IDLE_TTL_MS);
224
+ await Promise.allSettled(idle.map(([id, w]) => {
225
+ logger.info({ watchId: id, hostname: w.hostname }, 'Cleaning up idle log watch');
226
+ return this.destroyWatch(id);
227
+ }));
228
+ }
229
+ flushWaiters(w) {
230
+ if (w.pollWaiters.length === 0)
231
+ return;
232
+ const waiters = w.pollWaiters.splice(0);
233
+ for (const waiter of waiters) {
234
+ clearTimeout(waiter.timer);
235
+ waiter.resolve();
236
+ }
237
+ }
238
+ }
239
+ export function getLogWatchRegistry(context) {
240
+ const registry = context.serverContext?.logWatches;
241
+ if (!registry) {
242
+ throw new Error('Log watch registry not available');
243
+ }
244
+ return registry;
245
+ }
246
+ export function getLogWatchEntry(context, watchId) {
247
+ return getLogWatchRegistry(context).getWatchOrThrow(watchId);
248
+ }
249
+ //# sourceMappingURL=log-watch-registry.js.map
@@ -0,0 +1,9 @@
1
+ import { type GetRecentLogsOptions, type LogEntry } from '@salesforce/b2c-tooling-sdk/operations/logs';
2
+ import type { B2CInstance } from '@salesforce/b2c-tooling-sdk';
3
+ import type { McpTool } from '../../utils/index.js';
4
+ import type { Services } from '../../services.js';
5
+ import type { ServerContext } from '../../server-context.js';
6
+ export interface LogsGetRecentInjections {
7
+ getRecentLogs?: (instance: B2CInstance, options?: GetRecentLogsOptions) => Promise<LogEntry[]>;
8
+ }
9
+ export declare function createLogsGetRecentTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext, injections?: LogsGetRecentInjections): McpTool;
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { z } from 'zod';
7
+ import { filterByLevel, filterBySearch, filterBySince, getRecentLogs, parseSinceTime, } from '@salesforce/b2c-tooling-sdk/operations/logs';
8
+ import { createToolAdapter, jsonResult } from '../adapter.js';
9
+ const DEFAULT_PREFIXES = ['error', 'customerror'];
10
+ const DEFAULT_COUNT = 50;
11
+ export function createLogsGetRecentTool(loadServices, serverContext, injections) {
12
+ const getRecentLogsFn = injections?.getRecentLogs ?? getRecentLogs;
13
+ return createToolAdapter({
14
+ name: 'logs_get_recent',
15
+ description: 'Fetch recent log entries from the configured B2C Commerce instance in a single request/response. ' +
16
+ 'Best for quick lookups of the most recent errors. For monitoring across an action you trigger, ' +
17
+ 'use logs_watch_start + logs_watch_poll instead so entries are not missed between calls. ' +
18
+ 'Filters (since, level, search) are applied client-side after fetching.',
19
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
20
+ requiresInstance: true,
21
+ inputSchema: {
22
+ prefixes: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe('Log prefixes to read. Defaults to ["error", "customerror"].'),
26
+ count: z.number().int().positive().optional().describe('Maximum number of entries to return. Defaults to 50.'),
27
+ since: z
28
+ .string()
29
+ .optional()
30
+ .describe('Only entries after this time. Accepts relative ("5m", "1h", "2d") or ISO 8601.'),
31
+ level: z
32
+ .array(z.string())
33
+ .optional()
34
+ .describe('Filter by log level (ERROR, WARN, INFO, DEBUG, FATAL, TRACE). Case-insensitive.'),
35
+ search: z
36
+ .string()
37
+ .optional()
38
+ .describe('Case-insensitive substring filter applied to entry message and raw text.'),
39
+ },
40
+ async execute(args, context) {
41
+ const prefixes = args.prefixes ?? DEFAULT_PREFIXES;
42
+ const count = args.count ?? DEFAULT_COUNT;
43
+ let sinceDate;
44
+ if (args.since) {
45
+ sinceDate = parseSinceTime(args.since);
46
+ }
47
+ const needsClientFilter = Boolean(sinceDate || args.level?.length || args.search);
48
+ const fetchCount = needsClientFilter ? Math.max(count * 5, 500) : count;
49
+ let entries = await getRecentLogsFn(context.b2cInstance, {
50
+ prefixes,
51
+ maxEntries: fetchCount,
52
+ });
53
+ if (sinceDate) {
54
+ entries = filterBySince(entries, sinceDate);
55
+ }
56
+ if (args.level && args.level.length > 0) {
57
+ entries = filterByLevel(entries, args.level);
58
+ }
59
+ if (args.search) {
60
+ entries = filterBySearch(entries, args.search);
61
+ }
62
+ entries = entries.slice(0, count);
63
+ return { count: entries.length, entries };
64
+ },
65
+ formatOutput: (output) => jsonResult(output),
66
+ }, loadServices, serverContext);
67
+ }
68
+ //# sourceMappingURL=logs-get-recent.js.map
@@ -0,0 +1,9 @@
1
+ import { type ListLogsOptions, type LogFile } from '@salesforce/b2c-tooling-sdk/operations/logs';
2
+ import type { B2CInstance } from '@salesforce/b2c-tooling-sdk';
3
+ import type { McpTool } from '../../utils/index.js';
4
+ import type { Services } from '../../services.js';
5
+ import type { ServerContext } from '../../server-context.js';
6
+ export interface LogsListFilesInjections {
7
+ listLogFiles?: (instance: B2CInstance, options?: ListLogsOptions) => Promise<LogFile[]>;
8
+ }
9
+ export declare function createLogsListFilesTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext, injections?: LogsListFilesInjections): McpTool;
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { z } from 'zod';
7
+ import { listLogFiles } from '@salesforce/b2c-tooling-sdk/operations/logs';
8
+ import { createToolAdapter, jsonResult } from '../adapter.js';
9
+ export function createLogsListFilesTool(loadServices, serverContext, injections) {
10
+ const listLogFilesFn = injections?.listLogFiles ?? listLogFiles;
11
+ return createToolAdapter({
12
+ name: 'logs_list_files',
13
+ description: 'List log files on the configured B2C Commerce instance via WebDAV. ' +
14
+ 'Use this to discover what log prefixes are active (error, customerror, debug, jobs, ...) before fetching entries with logs_get_recent or starting a watch.',
15
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
16
+ requiresInstance: true,
17
+ inputSchema: {
18
+ prefixes: z
19
+ .array(z.string())
20
+ .optional()
21
+ .describe('Filter by log prefixes (e.g., ["error", "customerror"]). Returns all when omitted.'),
22
+ sort_by: z.enum(['date', 'name', 'size']).optional().describe('Sort field. Defaults to "date".'),
23
+ sort_order: z.enum(['asc', 'desc']).optional().describe('Sort order. Defaults to "desc".'),
24
+ },
25
+ async execute(args, context) {
26
+ const files = await listLogFilesFn(context.b2cInstance, {
27
+ prefixes: args.prefixes,
28
+ sortBy: args.sort_by ?? 'date',
29
+ sortOrder: args.sort_order ?? 'desc',
30
+ });
31
+ return { count: files.length, files };
32
+ },
33
+ formatOutput: (output) => jsonResult(output),
34
+ }, loadServices, serverContext);
35
+ }
36
+ //# sourceMappingURL=logs-list-files.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createLogsWatchListTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { createToolAdapter, jsonResult } from '../adapter.js';
7
+ import { getLogWatchRegistry } from './log-watch-registry.js';
8
+ export function createLogsWatchListTool(loadServices, serverContext) {
9
+ return createToolAdapter({
10
+ name: 'logs_watch_list',
11
+ description: 'List all active log watches with their status. Use this to find an orphaned watch_id (e.g., after a ' +
12
+ 'crashed agent run) so you can resume polling or stop it.',
13
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
14
+ inputSchema: {},
15
+ async execute(_args, context) {
16
+ const registry = getLogWatchRegistry(context);
17
+ const entries = registry.listWatches();
18
+ return {
19
+ watches: entries.map((w) => ({
20
+ buffered_entries: w.buffer.length,
21
+ created_at: new Date(w.createdAt).toISOString(),
22
+ dropped_entries: w.droppedEntries,
23
+ files_discovered: w.filesDiscovered,
24
+ hostname: w.hostname,
25
+ last_activity_at: new Date(w.lastActivityAt).toISOString(),
26
+ prefixes: w.prefixes,
27
+ stopped: w.stopped,
28
+ total_entries_seen: w.totalEntriesSeen,
29
+ watch_id: w.watchId,
30
+ })),
31
+ };
32
+ },
33
+ formatOutput: (output) => jsonResult(output),
34
+ }, loadServices, serverContext);
35
+ }
36
+ //# sourceMappingURL=logs-watch-list.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createLogsWatchPollTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { getLogWatchRegistry } from './log-watch-registry.js';
9
+ const DEFAULT_TIMEOUT_MS = 5000;
10
+ const DEFAULT_MAX_ENTRIES = 200;
11
+ export function createLogsWatchPollTool(loadServices, serverContext) {
12
+ return createToolAdapter({
13
+ name: 'logs_watch_poll',
14
+ description: 'Drain buffered entries from a log watch. If the buffer is empty, blocks up to timeout_ms waiting for ' +
15
+ 'new entries. Returns immediately if entries are already buffered. Set truncated=true if there are more ' +
16
+ 'entries beyond max_entries — call again to get the rest.',
17
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
18
+ inputSchema: {
19
+ watch_id: z.string().describe('Watch id from logs_watch_start.'),
20
+ timeout_ms: z
21
+ .number()
22
+ .int()
23
+ .min(0)
24
+ .optional()
25
+ .describe('Max time to block waiting for new entries when buffer is empty. Defaults to 5000ms.'),
26
+ max_entries: z
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .optional()
31
+ .describe('Maximum entries to return per call. Defaults to 200.'),
32
+ },
33
+ async execute(args, context) {
34
+ const registry = getLogWatchRegistry(context);
35
+ const watch = registry.getWatchOrThrow(args.watch_id);
36
+ const timeoutMs = args.timeout_ms ?? DEFAULT_TIMEOUT_MS;
37
+ const maxEntries = args.max_entries ?? DEFAULT_MAX_ENTRIES;
38
+ if (watch.buffer.length === 0 &&
39
+ watch.rotations.length === 0 &&
40
+ watch.errors.length === 0 &&
41
+ !watch.stopped &&
42
+ timeoutMs > 0) {
43
+ await registry.waitForActivity(args.watch_id, timeoutMs);
44
+ }
45
+ const drained = registry.drain(args.watch_id, maxEntries);
46
+ const droppedSnapshot = watch.droppedEntries;
47
+ watch.droppedEntries = 0;
48
+ return {
49
+ buffered_remaining: watch.buffer.length,
50
+ dropped_entries: droppedSnapshot,
51
+ entries: drained.entries,
52
+ errors: drained.errors,
53
+ files_discovered: drained.filesDiscovered,
54
+ files_rotated: drained.rotations,
55
+ stopped: watch.stopped,
56
+ truncated: drained.truncated,
57
+ watch_id: args.watch_id,
58
+ };
59
+ },
60
+ formatOutput: (output) => jsonResult(output),
61
+ }, loadServices, serverContext);
62
+ }
63
+ //# sourceMappingURL=logs-watch-poll.js.map
@@ -0,0 +1,9 @@
1
+ import { type TailLogsOptions, type TailLogsResult } from '@salesforce/b2c-tooling-sdk/operations/logs';
2
+ import type { B2CInstance } from '@salesforce/b2c-tooling-sdk';
3
+ import type { McpTool } from '../../utils/index.js';
4
+ import type { Services } from '../../services.js';
5
+ import type { ServerContext } from '../../server-context.js';
6
+ export interface LogsWatchStartInjections {
7
+ tailLogs?: (instance: B2CInstance, options?: TailLogsOptions) => Promise<TailLogsResult>;
8
+ }
9
+ export declare function createLogsWatchStartTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext, injections?: LogsWatchStartInjections): McpTool;
@@ -0,0 +1,117 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { z } from 'zod';
7
+ import { matchesLevel, matchesSearch, tailLogs, } from '@salesforce/b2c-tooling-sdk/operations/logs';
8
+ import { createToolAdapter, jsonResult } from '../adapter.js';
9
+ import { getLogWatchRegistry } from './log-watch-registry.js';
10
+ const DEFAULT_PREFIXES = ['error', 'customerror'];
11
+ export function createLogsWatchStartTool(loadServices, serverContext, injections) {
12
+ const tailLogsFn = injections?.tailLogs ?? tailLogs;
13
+ return createToolAdapter({
14
+ name: 'logs_watch_start',
15
+ description: 'Start a background log watch on the configured B2C Commerce instance. Returns a watch_id immediately. ' +
16
+ 'Recommended workflow: call logs_watch_start BEFORE triggering the action that should produce logs ' +
17
+ '(e.g., a storefront request, a job, a debug session). Then call logs_watch_poll to drain buffered ' +
18
+ 'entries (it blocks up to timeout_ms). Always call logs_watch_stop when done. ' +
19
+ 'Only one active watch per hostname at a time — use logs_watch_list to find an existing one.',
20
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
21
+ requiresInstance: true,
22
+ inputSchema: {
23
+ prefixes: z
24
+ .array(z.string())
25
+ .optional()
26
+ .describe('Log prefixes to watch. Defaults to ["error", "customerror"].'),
27
+ last_entries: z
28
+ .number()
29
+ .int()
30
+ .min(0)
31
+ .optional()
32
+ .describe('Number of pre-existing entries per file to emit on startup. Defaults to 0 so a fresh ' +
33
+ 'watch only captures NEW entries (matches the recommended "start before triggering" workflow). ' +
34
+ 'Set >0 to include recent context.'),
35
+ poll_interval_ms: z
36
+ .number()
37
+ .int()
38
+ .positive()
39
+ .optional()
40
+ .describe('How often the underlying tail polls the WebDAV server. Defaults to 3000ms.'),
41
+ level: z
42
+ .array(z.string())
43
+ .optional()
44
+ .describe('Server-side filter by log level. Entries not matching are dropped before buffering.'),
45
+ search: z.string().optional().describe('Case-insensitive substring filter applied as entries arrive.'),
46
+ },
47
+ async execute(args, context) {
48
+ const registry = getLogWatchRegistry(context);
49
+ const prefixes = args.prefixes ?? DEFAULT_PREFIXES;
50
+ const hostname = context.b2cInstance.config.hostname;
51
+ // Reserve the watchId on the registry by tracking the hostname check first
52
+ const existing = registry.findByHostname(hostname);
53
+ if (existing) {
54
+ throw new Error(`A log watch already exists for ${hostname} (watch_id: "${existing.watchId}"). ` +
55
+ `Stop it with logs_watch_stop first, or poll the existing watch.`);
56
+ }
57
+ const levelFilter = args.level;
58
+ const searchFilter = args.search;
59
+ // We need the watchId before tailLogs starts emitting, but we can only mint
60
+ // the id after registerWatch (which itself needs the tailResult). Use a holder
61
+ // so the callbacks below close over a mutable slot that we fill in after register.
62
+ const ref = {};
63
+ const tailResult = await tailLogsFn(context.b2cInstance, {
64
+ prefixes,
65
+ pollInterval: args.poll_interval_ms ?? 3000,
66
+ lastEntries: args.last_entries ?? 0,
67
+ onEntry(entry) {
68
+ if (!ref.id)
69
+ return;
70
+ if (levelFilter && levelFilter.length > 0 && !matchesLevel(entry, levelFilter)) {
71
+ return;
72
+ }
73
+ if (searchFilter && !matchesSearch(entry, searchFilter)) {
74
+ return;
75
+ }
76
+ registry.appendEntry(ref.id, entry);
77
+ },
78
+ onFileDiscovered(file) {
79
+ if (!ref.id)
80
+ return;
81
+ registry.appendFileDiscovered(ref.id, file);
82
+ },
83
+ onFileRotated(file) {
84
+ if (!ref.id)
85
+ return;
86
+ registry.appendRotation(ref.id, file);
87
+ },
88
+ onError(err) {
89
+ if (!ref.id)
90
+ return;
91
+ registry.appendError(ref.id, err);
92
+ },
93
+ });
94
+ // registerWatch re-checks the hostname and throws on a duplicate. If two
95
+ // logs_watch_start calls race past the findByHostname check above, the
96
+ // loser's tailLogs poll is already running in the background — stop it so
97
+ // it isn't orphaned (it would otherwise poll WebDAV until process exit).
98
+ let entry;
99
+ try {
100
+ entry = registry.registerWatch({ hostname, prefixes, tailResult });
101
+ }
102
+ catch (error) {
103
+ await tailResult.stop().catch(() => { });
104
+ throw error;
105
+ }
106
+ ref.id = entry.watchId;
107
+ return {
108
+ hostname,
109
+ prefixes,
110
+ started_at: new Date(entry.createdAt).toISOString(),
111
+ watch_id: entry.watchId,
112
+ };
113
+ },
114
+ formatOutput: (output) => jsonResult(output),
115
+ }, loadServices, serverContext);
116
+ }
117
+ //# sourceMappingURL=logs-watch-start.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ import type { ServerContext } from '../../server-context.js';
4
+ export declare function createLogsWatchStopTool(loadServices: () => Promise<Services> | Services, serverContext?: ServerContext): McpTool;
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { z } from 'zod';
7
+ import { createToolAdapter, jsonResult } from '../adapter.js';
8
+ import { getLogWatchRegistry } from './log-watch-registry.js';
9
+ export function createLogsWatchStopTool(loadServices, serverContext) {
10
+ return createToolAdapter({
11
+ name: 'logs_watch_stop',
12
+ description: 'Stop a log watch and release its resources. Idempotent on already-stopped watches (returns 200 with stopped_at). ' +
13
+ 'Always pair with logs_watch_start so polling state does not accumulate on the server.',
14
+ toolsets: ['CARTRIDGES', 'DIAGNOSTICS', 'SCAPI'],
15
+ inputSchema: {
16
+ watch_id: z.string().describe('Watch id from logs_watch_start.'),
17
+ },
18
+ async execute(args, context) {
19
+ const registry = getLogWatchRegistry(context);
20
+ // Idempotent: a watch that was already stopped (and removed) returns a
21
+ // success response rather than throwing, so retry/cleanup paths are safe.
22
+ const watch = registry.getWatch(args.watch_id);
23
+ const totalSeen = watch?.totalEntriesSeen ?? 0;
24
+ if (watch) {
25
+ await registry.destroyWatch(args.watch_id);
26
+ }
27
+ return {
28
+ stopped_at: new Date().toISOString(),
29
+ total_entries_seen: totalSeen,
30
+ watch_id: args.watch_id,
31
+ };
32
+ },
33
+ formatOutput: (output) => jsonResult(output),
34
+ }, loadServices, serverContext);
35
+ }
36
+ //# sourceMappingURL=logs-watch-stop.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../../utils/index.js';
2
+ import type { Services } from '../../services.js';
3
+ export declare function createDocsListTool(loadServices: () => Promise<Services> | Services): McpTool;