Skip to content

Commit f799280

Browse files
committed
Add MCP tools for log inspection and Script API/XSD docs
- Logs: logs_list_files, logs_get_recent, plus a watch lifecycle (logs_watch_start/poll/stop/list) that buffers entries between polls via a new LogWatchRegistry on ServerContext, so agents don't miss entries produced between request/response turns. - Docs: docs_search/read/list and docs_schema_search/read/list over the bundled Script API and XSD corpora; no instance auth required. - Adds a new DIAGNOSTICS toolset that groups the script debugger and log tools; existing debug-* tools also re-tagged. - Promotes log filter helpers (parseSinceTime, filterBy*, matchesLevel, matchesSearch) from the CLI to @salesforce/b2c-tooling-sdk so the MCP package can reuse them.
1 parent ac0da1b commit f799280

40 files changed

Lines changed: 2180 additions & 136 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@salesforce/b2c-dx-mcp': minor
3+
'@salesforce/b2c-tooling-sdk': minor
4+
---
5+
6+
Add MCP tools for log inspection and documentation lookup. Logs: `logs_list_files`, `logs_get_recent`, and a `logs_watch_start` / `logs_watch_poll` / `logs_watch_stop` / `logs_watch_list` lifecycle that buffers entries between polls so agents don't miss logs produced between tool calls. Docs: `docs_search`, `docs_read`, `docs_list`, `docs_schema_search`, `docs_schema_read`, `docs_schema_list` for the bundled Script API and XSD schema corpora. Adds a new `DIAGNOSTICS` toolset that groups the script debugger and log tools. SDK now also exports the log filter helpers (`parseSinceTime`, `filterBySince`, `filterByLevel`, `filterBySearch`, `matchesLevel`, `matchesSearch`) for reuse.

docs/.vitepress/config.mts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,15 @@ const referenceSidebar = [
165165
{
166166
text: 'Diagnostics',
167167
collapsed: true,
168-
items: [{text: 'Script Debugger', link: '/mcp/tools/diagnostics'}],
168+
items: [
169+
{text: 'Script Debugger', link: '/mcp/tools/diagnostics'},
170+
{text: 'Logs', link: '/mcp/tools/logs'},
171+
],
172+
},
173+
{
174+
text: 'Documentation',
175+
collapsed: true,
176+
items: [{text: 'Script API & Schemas', link: '/mcp/tools/docs'}],
169177
},
170178
],
171179
},

docs/mcp/tools/docs.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
description: MCP tools for searching B2C Commerce Script API and XSD schema documentation.
3+
---
4+
5+
# Documentation Tools
6+
7+
MCP tools for searching and reading the bundled B2C Commerce Script API documentation and XSD schemas. These tools read data shipped with the SDK — they do **not** require any instance configuration or authentication. Available in **every toolset**.
8+
9+
## When to use which tool
10+
11+
- Look up a class or module — search first to confirm the id, then read.
12+
- `docs_search` (fuzzy) → `docs_read` (full markdown).
13+
- Look up an XSD schema for an import file (e.g., `system-objecttype-extensions.xml`) — same pattern.
14+
- `docs_schema_search``docs_schema_read`.
15+
- Browse all available entries — `docs_list` / `docs_schema_list`.
16+
17+
> **Note:** `docs_read` and `docs_schema_read` content can be large (full markdown files / full XSD bodies). Prefer `docs_search`/`docs_schema_search` to narrow down before reading.
18+
19+
---
20+
21+
## Script API
22+
23+
### docs_search
24+
25+
Fuzzy-search Script API documentation by class, module, or partial name.
26+
27+
| Parameter | Type | Required | Default | Description |
28+
|-----------|------|----------|---------|-------------|
29+
| `query` | string | Yes | | Search query (e.g., `"ProductMgr"`, `"dw.catalog"`, `"Status"`) |
30+
| `limit` | number | No | `20` | Maximum number of results |
31+
32+
**Returns:** `{query, results: [{entry: {id, title, filePath, preview?}, score}]}`. Lower `score` = better match.
33+
34+
### docs_read
35+
36+
Read full markdown documentation for a Script API class or module.
37+
38+
| Parameter | Type | Required | Description |
39+
|-----------|------|----------|-------------|
40+
| `query` | string | Yes | Exact id (e.g., `"dw.catalog.ProductMgr"`) or fuzzy query |
41+
42+
**Returns:** `{entry: {id, title, filePath, preview?}, content: string}`. Returns an error result with a hint to use `docs_search` if no match is found.
43+
44+
### docs_list
45+
46+
List every available Script API documentation entry.
47+
48+
No parameters.
49+
50+
**Returns:** `{count, entries: [{id, title, filePath, preview?}]}`. Output is large; prefer `docs_search` for targeted lookups.
51+
52+
---
53+
54+
## XSD Schemas
55+
56+
### docs_schema_search
57+
58+
Fuzzy-search XSD schema ids.
59+
60+
| Parameter | Type | Required | Default | Description |
61+
|-----------|------|----------|---------|-------------|
62+
| `query` | string | Yes | | Schema name or partial match (e.g., `"catalog"`, `"order"`) |
63+
| `limit` | number | No | `20` | Maximum number of results |
64+
65+
**Returns:** `{query, results: [{entry: {id, filePath}, score}]}`.
66+
67+
### docs_schema_read
68+
69+
Read the contents of an XSD schema (raw XML) plus the on-disk path.
70+
71+
| Parameter | Type | Required | Description |
72+
|-----------|------|----------|-------------|
73+
| `query` | string | Yes | Exact id or fuzzy query |
74+
75+
**Returns:** `{entry: {id, filePath}, content: string, path: string}`. Returns an error result with a hint to use `docs_schema_search` if no match.
76+
77+
### docs_schema_list
78+
79+
List every available XSD schema id.
80+
81+
No parameters.
82+
83+
**Returns:** `{count, entries: [{id, filePath}]}`.
84+
85+
---
86+
87+
## See also
88+
89+
- [`b2c docs search`](../../cli/docs/search) — CLI search
90+
- [`b2c docs read`](../../cli/docs/read) — CLI read
91+
- [`b2c docs schema`](../../cli/docs/schema) — CLI XSD reader

docs/mcp/tools/logs.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
description: MCP tools for fetching and tailing logs on B2C Commerce instances.
3+
---
4+
5+
# Log Tools
6+
7+
MCP tools for inspecting runtime logs on a B2C Commerce instance via WebDAV. Use them to investigate errors after triggering a request, monitor a job run, or audit recent failures. Available in the **CARTRIDGES**, **DIAGNOSTICS**, and **SCAPI** toolsets.
8+
9+
## Authentication
10+
11+
All log tools that read from the instance (`logs_list_files`, `logs_get_recent`, `logs_watch_start`) require WebDAV-capable credentials.
12+
13+
**Required:**
14+
- **Basic Auth** preferred: `hostname`, `username`, and `password` (WebDAV access key) for a Business Manager user with WebDAV log read permission.
15+
- OAuth (client-credentials / implicit) is also supported as a fallback for WebDAV.
16+
17+
**Configuration priority:** Flags → Environment variables → `dw.json` config file
18+
19+
`logs_watch_poll`, `logs_watch_stop`, and `logs_watch_list` operate on server-side state only and do not require fresh credentials per call.
20+
21+
See [Configuration](../configuration) for credential setup.
22+
23+
## When to use which tool
24+
25+
- Quick lookup of recent errors → **`logs_get_recent`**.
26+
- Discover what log prefixes are active on the instance → **`logs_list_files`**.
27+
- Monitor logs across a multi-step action you trigger (storefront request, job, debug session) → start a watch with **`logs_watch_start`**, then drain it with **`logs_watch_poll`**, and finish with **`logs_watch_stop`**. The watch buffers entries between calls so nothing is lost between agent turns.
28+
29+
## Recovery from orphaned watches
30+
31+
Log watches are stateful and live in the MCP server process. Only one watch is allowed per hostname. If an agent loses track of an active watch:
32+
33+
1. **List active watches** — call `logs_watch_list` (no args). It returns all watches with their `watch_id`, `hostname`, `prefixes`, and buffered counts.
34+
2. **Stop orphaned watches** — call `logs_watch_stop` with the `watch_id` to release the underlying tail.
35+
3. **Idle cleanup** — watches inactive for 30 minutes are automatically destroyed.
36+
4. **Restart the MCP server** — last resort; destroys all watch state.
37+
38+
---
39+
40+
## One-shot tools
41+
42+
### logs_list_files
43+
44+
List log files on the instance via WebDAV.
45+
46+
| Parameter | Type | Required | Default | Description |
47+
|-----------|------|----------|---------|-------------|
48+
| `prefixes` | string[] | No | all | Filter by log prefix (e.g., `["error", "customerror"]`) |
49+
| `sort_by` | `"date" \| "name" \| "size"` | No | `date` | Sort field |
50+
| `sort_order` | `"asc" \| "desc"` | No | `desc` | Sort order |
51+
52+
**Returns:** `{count, files: [{name, prefix, size, lastModified, path}]}`.
53+
54+
### logs_get_recent
55+
56+
Fetch recent log entries in a single request/response. Filters (`since`, `level`, `search`) are applied client-side after fetching.
57+
58+
| Parameter | Type | Required | Default | Description |
59+
|-----------|------|----------|---------|-------------|
60+
| `prefixes` | string[] | No | `["error", "customerror"]` | Log prefixes to read |
61+
| `count` | number | No | `50` | Maximum entries to return |
62+
| `since` | string | No | | Relative time (`"5m"`, `"1h"`, `"2d"`) or ISO 8601 |
63+
| `level` | string[] | No | | Filter by level (ERROR, WARN, INFO, DEBUG, FATAL, TRACE) |
64+
| `search` | string | No | | Case-insensitive substring filter |
65+
66+
**Returns:** `{count, entries: [{file, level, timestamp, message, raw}]}`.
67+
68+
---
69+
70+
## Watch lifecycle
71+
72+
### logs_watch_start
73+
74+
Start a background log watch. Returns a `watch_id` immediately. Buffers entries in memory until `logs_watch_poll` drains them.
75+
76+
> **Workflow:** call `logs_watch_start` **before** triggering the action that should produce logs. Otherwise startup may emit only existing entries (controlled by `last_entries`) and miss what you wanted to capture.
77+
78+
| Parameter | Type | Required | Default | Description |
79+
|-----------|------|----------|---------|-------------|
80+
| `prefixes` | string[] | No | `["error", "customerror"]` | Log prefixes to watch |
81+
| `last_entries` | number | No | `1` | Recent entries per file to emit on startup. `0` skips. |
82+
| `poll_interval_ms` | number | No | `3000` | How often the underlying tail polls WebDAV |
83+
| `level` | string[] | No | | Drop entries not matching level before buffering |
84+
| `search` | string | No | | Drop entries not matching substring before buffering |
85+
86+
**Returns:** `{watch_id, hostname, prefixes, started_at}`.
87+
88+
### logs_watch_poll
89+
90+
Drain buffered entries. If the buffer is empty, blocks up to `timeout_ms` waiting for new entries.
91+
92+
| Parameter | Type | Required | Default | Description |
93+
|-----------|------|----------|---------|-------------|
94+
| `watch_id` | string | Yes | | Watch id from `logs_watch_start` |
95+
| `timeout_ms` | number | No | `5000` | Max time to block when buffer is empty. `0` returns immediately. |
96+
| `max_entries` | number | No | `200` | Maximum entries returned per call |
97+
98+
**Returns:** `{watch_id, entries, files_discovered, files_rotated, errors, truncated, buffered_remaining, dropped_entries, stopped}`. When `truncated` is `true`, call again to drain the rest.
99+
100+
### logs_watch_stop
101+
102+
Stop a watch and release its underlying tail.
103+
104+
| Parameter | Type | Required | Description |
105+
|-----------|------|----------|-------------|
106+
| `watch_id` | string | Yes | Watch id from `logs_watch_start` |
107+
108+
**Returns:** `{watch_id, stopped_at, total_entries_seen}`.
109+
110+
### logs_watch_list
111+
112+
List active watches. Use to recover orphaned watches or inspect buffered counts.
113+
114+
No parameters.
115+
116+
**Returns:** `{watches: [{watch_id, hostname, prefixes, buffered_entries, total_entries_seen, dropped_entries, files_discovered, stopped, created_at, last_activity_at}]}`.
117+
118+
---
119+
120+
## See also
121+
122+
- [`b2c logs tail`](../../cli/logs/tail) — interactive CLI tail
123+
- [`b2c logs get`](../../cli/logs/get) — one-shot CLI fetch
124+
- [`b2c logs list`](../../cli/logs/list) — list log files from the CLI

packages/b2c-cli/src/utils/logs/filter.ts

Lines changed: 10 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -4,121 +4,13 @@
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
66

7-
import type {LogEntry} from '@salesforce/b2c-tooling-sdk/operations/logs';
8-
9-
/**
10-
* Parses a relative time string (e.g., "5m", "1h", "2d") into milliseconds.
11-
* Returns null if the string is not a valid relative time format.
12-
*/
13-
export function parseRelativeTime(timeStr: string): null | number {
14-
const match = timeStr.match(/^(\d+)([mhd])$/i);
15-
if (!match) {
16-
return null;
17-
}
18-
19-
const value = Number.parseInt(match[1], 10);
20-
const unit = match[2].toLowerCase();
21-
22-
switch (unit) {
23-
case 'd': {
24-
return value * 24 * 60 * 60 * 1000;
25-
}
26-
case 'h': {
27-
return value * 60 * 60 * 1000;
28-
}
29-
case 'm': {
30-
return value * 60 * 1000;
31-
}
32-
default: {
33-
return null;
34-
}
35-
}
36-
}
37-
38-
/**
39-
* Parses a --since value into a Date object.
40-
* Supports:
41-
* - Relative times: "5m", "1h", "2d"
42-
* - ISO 8601: "2026-01-25T10:00:00"
43-
*/
44-
export function parseSinceTime(sinceStr: string): Date {
45-
// Try relative time first
46-
const relativeMs = parseRelativeTime(sinceStr);
47-
if (relativeMs !== null) {
48-
return new Date(Date.now() - relativeMs);
49-
}
50-
51-
// Try ISO 8601
52-
const date = new Date(sinceStr);
53-
if (Number.isNaN(date.getTime())) {
54-
throw new TypeError(
55-
`Invalid --since value: "${sinceStr}". Use relative time (e.g., "5m", "1h", "2d") or ISO 8601 (e.g., "2026-01-25T10:00:00")`,
56-
);
57-
}
58-
59-
return date;
60-
}
61-
62-
/**
63-
* Parses a B2C log timestamp into a Date object.
64-
* Expected format: "2025-01-25 10:30:45.123 GMT"
65-
*/
66-
export function parseLogTimestamp(timestamp: string): Date | null {
67-
// B2C format: "2025-01-25 10:30:45.123 GMT"
68-
// Convert to ISO format for parsing
69-
const isoFormat = timestamp.replace(' GMT', 'Z').replace(' ', 'T');
70-
const date = new Date(isoFormat);
71-
return Number.isNaN(date.getTime()) ? null : date;
72-
}
73-
74-
/**
75-
* Filters entries by timestamp.
76-
*/
77-
export function filterBySince(entries: LogEntry[], since: Date): LogEntry[] {
78-
return entries.filter((entry) => {
79-
if (!entry.timestamp) return true; // Include entries without timestamps
80-
const entryDate = parseLogTimestamp(entry.timestamp);
81-
return entryDate === null || entryDate >= since;
82-
});
83-
}
84-
85-
/**
86-
* Filters entries by log level.
87-
*/
88-
export function filterByLevel(entries: LogEntry[], levels: string[]): LogEntry[] {
89-
const upperLevels = new Set(levels.map((l) => l.toUpperCase()));
90-
return entries.filter((entry) => {
91-
// Include entries without level if no specific level filter
92-
if (!entry.level) return false;
93-
return upperLevels.has(entry.level.toUpperCase());
94-
});
95-
}
96-
97-
/**
98-
* Filters entries by text search (case-insensitive substring match).
99-
*/
100-
export function filterBySearch(entries: LogEntry[], search: string): LogEntry[] {
101-
const lowerSearch = search.toLowerCase();
102-
return entries.filter((entry) => {
103-
return entry.message.toLowerCase().includes(lowerSearch) || entry.raw.toLowerCase().includes(lowerSearch);
104-
});
105-
}
106-
107-
/**
108-
* Checks if a single entry matches the specified log levels.
109-
* Used for streaming/tail scenarios where we filter one entry at a time.
110-
*/
111-
export function matchesLevel(entry: LogEntry, levels: string[]): boolean {
112-
if (!entry.level) return false;
113-
const upperLevels = new Set(levels.map((l) => l.toUpperCase()));
114-
return upperLevels.has(entry.level.toUpperCase());
115-
}
116-
117-
/**
118-
* Checks if a single entry matches the search text (case-insensitive).
119-
* Used for streaming/tail scenarios where we filter one entry at a time.
120-
*/
121-
export function matchesSearch(entry: LogEntry, search: string): boolean {
122-
const lowerSearch = search.toLowerCase();
123-
return entry.message.toLowerCase().includes(lowerSearch) || entry.raw.toLowerCase().includes(lowerSearch);
124-
}
7+
export {
8+
filterByLevel,
9+
filterBySearch,
10+
filterBySince,
11+
matchesLevel,
12+
matchesSearch,
13+
parseLogTimestamp,
14+
parseRelativeTime,
15+
parseSinceTime,
16+
} from '@salesforce/b2c-tooling-sdk/operations/logs';

packages/b2c-dx-mcp/src/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {Services} from './services.js';
1313
import type {ServerContext} from './server-context.js';
1414
import {createCartridgesTools} from './tools/cartridges/index.js';
1515
import {createDiagnosticsTools} from './tools/diagnostics/index.js';
16+
import {createDocsTools} from './tools/docs/index.js';
1617
import {createMrtTools} from './tools/mrt/index.js';
1718
import {createPwav3Tools} from './tools/pwav3/index.js';
1819
import {createScapiTools} from './tools/scapi/index.js';
@@ -87,6 +88,7 @@ export function createToolRegistry(
8788
): ToolRegistry {
8889
const registry: ToolRegistry = {
8990
CARTRIDGES: [],
91+
DIAGNOSTICS: [],
9092
MRT: [],
9193
PWAV3: [],
9294
SCAPI: [],
@@ -97,6 +99,7 @@ export function createToolRegistry(
9799
const allTools: McpTool[] = [
98100
...createCartridgesTools(loadServices),
99101
...createDiagnosticsTools(loadServices, serverContext),
102+
...createDocsTools(loadServices),
100103
...createMrtTools(loadServices),
101104
...createPwav3Tools(loadServices),
102105
...createScapiTools(loadServices),

0 commit comments

Comments
 (0)