@salesforce/graphiti 10.15.1 → 10.16.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.
- package/CHANGELOG.md +6 -0
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/mcp-mirror/commands.d.ts +16 -0
- package/dist/commands/mcp-mirror/commands.js +38 -0
- package/dist/commands/mcp-mirror/commands.js.map +1 -0
- package/dist/commands/mcp-mirror/run-mirror.d.ts +42 -0
- package/dist/commands/mcp-mirror/run-mirror.js +105 -0
- package/dist/commands/mcp-mirror/run-mirror.js.map +1 -0
- package/dist/lib/graphql-name.d.ts +2 -2
- package/dist/lib/graphql-name.js +2 -2
- package/dist/mcp/tools/sf-gql-aggregate.js +2 -55
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -1
- package/dist/mcp/tools/sf-gql-connect.js +2 -9
- package/dist/mcp/tools/sf-gql-connect.js.map +1 -1
- package/dist/mcp/tools/sf-gql-create.js +2 -15
- package/dist/mcp/tools/sf-gql-create.js.map +1 -1
- package/dist/mcp/tools/sf-gql-delete.js +2 -11
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -1
- package/dist/mcp/tools/sf-gql-detail.js +2 -30
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -1
- package/dist/mcp/tools/sf-gql-discover.js +2 -33
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -1
- package/dist/mcp/tools/sf-gql-list.js +2 -32
- package/dist/mcp/tools/sf-gql-list.js.map +1 -1
- package/dist/mcp/tools/sf-gql-raw.js +2 -19
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -1
- package/dist/mcp/tools/sf-gql-update.js +2 -15
- package/dist/mcp/tools/sf-gql-update.js.map +1 -1
- package/dist/{mcp/tools/shared/zod-schemas.js → schemas/fields.js} +2 -2
- package/dist/schemas/fields.js.map +1 -0
- package/dist/schemas/input-schemas.d.ts +318 -0
- package/dist/schemas/input-schemas.js +230 -0
- package/dist/schemas/input-schemas.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +23 -1
- package/src/commands/mcp-mirror/__tests__/commands.spec.ts +146 -0
- package/src/commands/mcp-mirror/__tests__/run-mirror.spec.ts +251 -0
- package/src/commands/mcp-mirror/commands.ts +92 -0
- package/src/commands/mcp-mirror/run-mirror.ts +149 -0
- package/src/lib/graphql-name.ts +2 -2
- package/src/mcp/tools/sf-gql-aggregate.ts +2 -68
- package/src/mcp/tools/sf-gql-connect.ts +2 -11
- package/src/mcp/tools/sf-gql-create.ts +2 -21
- package/src/mcp/tools/sf-gql-delete.ts +2 -15
- package/src/mcp/tools/sf-gql-detail.ts +2 -42
- package/src/mcp/tools/sf-gql-discover.ts +2 -36
- package/src/mcp/tools/sf-gql-list.ts +2 -41
- package/src/mcp/tools/sf-gql-raw.ts +2 -25
- package/src/mcp/tools/sf-gql-update.ts +2 -21
- package/src/{mcp/tools/shared/zod-schemas.ts → schemas/fields.ts} +1 -1
- package/src/schemas/input-schemas.ts +305 -0
- package/dist/mcp/tools/shared/zod-schemas.js.map +0 -1
- package/dist/{mcp/tools/shared/zod-schemas.d.ts → schemas/fields.d.ts} +2 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Per-tool Zod input schemas, extracted so the MCP server and the CLI mirror
|
|
8
|
+
* (`src/commands/mcp-mirror/`) validate identical argument shapes. The MCP
|
|
9
|
+
* tools consume `<TOOL>_INPUT.shape` (raw-shape registration) or the object
|
|
10
|
+
* directly (`detail`, which is `.strict()`); the CLI adapters call
|
|
11
|
+
* `<TOOL>_INPUT.parse()` on the incoming JSON. Single source of truth keeps
|
|
12
|
+
* the two transports arg-for-arg symmetric.
|
|
13
|
+
*
|
|
14
|
+
* Descriptions and validation are preserved verbatim from the original inline
|
|
15
|
+
* schemas; behavior is unchanged — this is a pure extraction.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { childRelationshipSchema, graphqlName, orderByObject, orgAlias } from "./fields.js";
|
|
19
|
+
import { GROUP_BY_FUNCTIONS } from "../intent/types.js";
|
|
20
|
+
// --- sf_gql_list ------------------------------------------------------------
|
|
21
|
+
// `sf_gql_list` advertises both shapes (singleton + array) for backward
|
|
22
|
+
// compatibility with early MCP clients that learned the array form.
|
|
23
|
+
const listOrderBySchema = z.union([orderByObject, z.array(orderByObject)]);
|
|
24
|
+
export const LIST_INPUT = z.object({
|
|
25
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
26
|
+
object: graphqlName('SObject API name, e.g. "Account", "Case". Must be a valid GraphQL Name.'),
|
|
27
|
+
fields: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.describe('Scalar field API names to select on the node. Dot-paths like "Owner.Name" are allowed.'),
|
|
30
|
+
parentFields: z
|
|
31
|
+
.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Dotted parent-relationship paths, e.g. "Account.Name".'),
|
|
34
|
+
childRelationships: z.array(childRelationshipSchema(listOrderBySchema)).optional(),
|
|
35
|
+
filter: z
|
|
36
|
+
.record(z.unknown())
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("<Object>_Filter shape. String leaves matching $varName promote to typed query variables."),
|
|
39
|
+
orderBy: listOrderBySchema
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("<Object>_OrderBy. Singleton object preferred; arrays are collapsed to the first entry."),
|
|
42
|
+
first: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.positive()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Top-level connection page size; defaults to 10."),
|
|
48
|
+
scope: z.string().optional().describe('Scope enum (e.g. "MINE", "EVERYTHING") or $varName.'),
|
|
49
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to <Object>List.").optional(),
|
|
50
|
+
});
|
|
51
|
+
// --- sf_gql_detail ----------------------------------------------------------
|
|
52
|
+
// Advertised schema per FR-6.3 is the singleton object form; we still accept
|
|
53
|
+
// arrays at runtime via `z.preprocess` (FR-6.2 compat shim for early MCP
|
|
54
|
+
// clients that learned the array shape). The transform collapses to the first
|
|
55
|
+
// element before validation, so downstream code sees a singleton.
|
|
56
|
+
const detailOrderBySchema = z.preprocess((v) => (Array.isArray(v) ? v[0] : v), orderByObject);
|
|
57
|
+
// Note: filter/orderBy/scope/first are intentionally absent at the top level —
|
|
58
|
+
// `sf_gql_detail` is single-record-by-Id (FR-5.5). Filtering happens via the
|
|
59
|
+
// declared `$<idVariable>: ID!`; pagination doesn't apply.
|
|
60
|
+
//
|
|
61
|
+
// `.strict()` rejects unknown top-level keys at the MCP boundary so an LLM
|
|
62
|
+
// hallucinating sibling-tool params (`filter`, `scope`, `first`) gets a
|
|
63
|
+
// validation error to learn from rather than silent omission.
|
|
64
|
+
export const DETAIL_INPUT = z
|
|
65
|
+
.object({
|
|
66
|
+
org: orgAlias("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
67
|
+
object: graphqlName('SObject API name, e.g. "Account", "Case". Must be a valid GraphQL Name.'),
|
|
68
|
+
fields: z
|
|
69
|
+
.array(z.string())
|
|
70
|
+
.describe('Scalar field API names to select on the node. Dot-paths like "Owner.Name" are allowed.'),
|
|
71
|
+
parentFields: z
|
|
72
|
+
.array(z.string())
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Dotted parent-relationship paths, e.g. "Account.Name".'),
|
|
75
|
+
childRelationships: z.array(childRelationshipSchema(detailOrderBySchema)).optional(),
|
|
76
|
+
idVariable: graphqlName('Name (without leading "$") for the ID variable. Defaults to "id". Declared as <name>: ID! and bound via where { Id: { eq: $<name> } } per FR-5.5. Choose a name that does not appear as a $varName in any childRelationships filter/orderBy — collisions are rejected.').optional(),
|
|
77
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to <Object>Detail.").optional(),
|
|
78
|
+
})
|
|
79
|
+
.strict();
|
|
80
|
+
// --- sf_gql_discover --------------------------------------------------------
|
|
81
|
+
// Org/object/field flow into shell commands (sf CLI), filesystem paths
|
|
82
|
+
// (ObjectInfo cache), and GraphQL operation names. Bound at the MCP boundary
|
|
83
|
+
// to safe API-name charsets so downstream layers can trust them.
|
|
84
|
+
const DISCOVER_ORG_ALIAS_RE = /^[A-Za-z0-9_-]{1,80}$/;
|
|
85
|
+
const DISCOVER_SOBJECT_NAME_RE = /^[A-Za-z][A-Za-z0-9_]{0,79}$/;
|
|
86
|
+
// eslint-disable-next-line no-control-regex -- intentionally rejects control chars
|
|
87
|
+
const DISCOVER_SEARCH_RE = /^[^\x00-\x1f\x7f]*$/;
|
|
88
|
+
export const DISCOVER_INPUT = z.object({
|
|
89
|
+
org: z
|
|
90
|
+
.string()
|
|
91
|
+
.regex(DISCOVER_ORG_ALIAS_RE, "org must match /^[A-Za-z0-9_-]{1,80}$/")
|
|
92
|
+
.describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
93
|
+
mode: z
|
|
94
|
+
.enum(["list_objects", "describe_object", "describe_field"])
|
|
95
|
+
.describe('Discovery mode. "list_objects" enumerates queryable SObjects; "describe_object" and "describe_field" return ObjectInfo metadata.'),
|
|
96
|
+
object: z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(DISCOVER_SOBJECT_NAME_RE, "object must match /^[A-Za-z][A-Za-z0-9_]{0,79}$/")
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('SObject API name. Required for "describe_object" and "describe_field".'),
|
|
101
|
+
field: z
|
|
102
|
+
.string()
|
|
103
|
+
.regex(DISCOVER_SOBJECT_NAME_RE, "field must match /^[A-Za-z][A-Za-z0-9_]{0,79}$/")
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Field API name. Required for "describe_field".'),
|
|
106
|
+
search: z
|
|
107
|
+
.string()
|
|
108
|
+
.max(100, "search must be 100 characters or fewer")
|
|
109
|
+
.regex(DISCOVER_SEARCH_RE, "search must not contain control characters")
|
|
110
|
+
.optional()
|
|
111
|
+
.describe('Optional substring filter applied to "list_objects" results.'),
|
|
112
|
+
});
|
|
113
|
+
// --- sf_gql_aggregate -------------------------------------------------------
|
|
114
|
+
const aliasField = z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("GraphQL alias for this aggregation's result key.");
|
|
118
|
+
const aggregationSchema = z.discriminatedUnion("function", [
|
|
119
|
+
z.object({
|
|
120
|
+
function: z.enum(["count", "countDistinct"]),
|
|
121
|
+
field: z.string().optional().describe('SObject field API name. Defaults to "Id" when omitted.'),
|
|
122
|
+
alias: aliasField,
|
|
123
|
+
}),
|
|
124
|
+
z.object({
|
|
125
|
+
function: z.enum(["sum", "avg", "min", "max"]),
|
|
126
|
+
field: z.string().describe("SObject field API name. Required for sum/avg/min/max."),
|
|
127
|
+
alias: aliasField,
|
|
128
|
+
}),
|
|
129
|
+
]);
|
|
130
|
+
const groupByElementSchema = z.union([
|
|
131
|
+
z.string(),
|
|
132
|
+
z.object({
|
|
133
|
+
field: z.string().describe("SObject field API name (DateTime/Date field)."),
|
|
134
|
+
function: z
|
|
135
|
+
.enum(GROUP_BY_FUNCTIONS)
|
|
136
|
+
.describe("Date bucketing function from UIAPI GroupByFunction enum."),
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
export const AGGREGATE_INPUT = z.object({
|
|
140
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
141
|
+
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
142
|
+
groupBy: z
|
|
143
|
+
.array(groupByElementSchema)
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("GroupBy elements. Each is either a plain field name string (renders as `{Field: {group: true}}`) or an object `{field, function}` for date-bucketed groupBy (renders as `{Field: {function: CALENDAR_MONTH}}`). Omit or pass `[]` for un-grouped aggregation; with no aggregations either, defaults to `count` over `Id` (FR-8.2)."),
|
|
146
|
+
aggregations: z
|
|
147
|
+
.array(aggregationSchema)
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("List of aggregation functions to compute. Each entry projects under its alias (or default `<function><PascalCaseField>`)."),
|
|
150
|
+
filter: z
|
|
151
|
+
.record(z.unknown())
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("<Object>_Filter applied to every aggregation. String leaves matching $varName promote to typed query variables."),
|
|
154
|
+
orderBy: z
|
|
155
|
+
.union([z.record(z.unknown()), z.array(z.record(z.unknown()))])
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("<Object>_OrderBy. Singleton object preferred; arrays are collapsed to the first entry. String leaves matching $varName promote to typed query variables."),
|
|
158
|
+
first: z
|
|
159
|
+
.number()
|
|
160
|
+
.int()
|
|
161
|
+
.positive()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Connection page size (top-N pattern). No default — omit for all buckets."),
|
|
164
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to <Object>Aggregate.").optional(),
|
|
165
|
+
});
|
|
166
|
+
// --- sf_gql_raw -------------------------------------------------------------
|
|
167
|
+
export const RAW_INPUT = z.object({
|
|
168
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
169
|
+
commands: z
|
|
170
|
+
.array(z.string())
|
|
171
|
+
.min(1)
|
|
172
|
+
.describe("CLI-style commands applied in order to a transient session. Supported verbs:\n" +
|
|
173
|
+
" select <path[:alias]> e.g. select uiapi/query/Case/edges/node/Subject/value\n" +
|
|
174
|
+
" set [<path>] <key>=<value> e.g. set uiapi/query/Case first=10 | set uiapi/query/Case where.Status=New\n" +
|
|
175
|
+
" var $name <path> [default] e.g. var $id uiapi/query/Case/@args/where/Id/eq (type inferred from path)\n" +
|
|
176
|
+
'Each command is tokenized on spaces and a value MUST NOT contain a space — quoting does not help once a token has started (key=\'a b\' still splits). A filter value that contains a space (e.g. "New York", "In Progress") cannot be expressed via set in v1; use sf_gql_list with a JSON filter, or pass the value through a variable bound with var.\n' +
|
|
177
|
+
"Fails fast: a bad command aborts the whole call. Other CLI verbs (cd, drop, alias, optional, unset) are NOT supported in v1."),
|
|
178
|
+
operation: z
|
|
179
|
+
.enum(["query", "mutation", "aggregate"])
|
|
180
|
+
.optional()
|
|
181
|
+
.describe('Operation root. "query" (default) → uiapi.query; "mutation" → mutation root; "aggregate" → uiapi.aggregate.'),
|
|
182
|
+
typeName: graphqlName("Override the GraphQL operation name. Defaults to Raw<Operation>.").optional(),
|
|
183
|
+
});
|
|
184
|
+
// --- sf_gql_create ----------------------------------------------------------
|
|
185
|
+
export const CREATE_INPUT = z.object({
|
|
186
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
187
|
+
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
188
|
+
returnFields: z
|
|
189
|
+
.array(z.string())
|
|
190
|
+
.optional()
|
|
191
|
+
.describe('Scalar field API names to read back on the created Record. Defaults to ["Id"]. Dot-paths like Owner.Name are not supported in mutation results; passing them produces a warning and the field is skipped.'),
|
|
192
|
+
inputVariable: z
|
|
193
|
+
.string()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('Variable name (without "$") for the create input. Defaults to "input". Declared as $<name>: <Object>CreateInput!.'),
|
|
196
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to Create<Object>.").optional(),
|
|
197
|
+
});
|
|
198
|
+
// --- sf_gql_update ----------------------------------------------------------
|
|
199
|
+
export const UPDATE_INPUT = z.object({
|
|
200
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
201
|
+
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
202
|
+
returnFields: z
|
|
203
|
+
.array(z.string())
|
|
204
|
+
.optional()
|
|
205
|
+
.describe('Scalar field API names to read back on the updated Record. Defaults to ["Id"]. Dot-paths like Owner.Name are not supported in mutation results; passing them produces a warning and the field is skipped.'),
|
|
206
|
+
inputVariable: z
|
|
207
|
+
.string()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe('Variable name (without "$") for the update input. Defaults to "input". Declared as $<name>: <Object>UpdateInput!.'),
|
|
210
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to Update<Object>.").optional(),
|
|
211
|
+
});
|
|
212
|
+
// --- sf_gql_delete ----------------------------------------------------------
|
|
213
|
+
export const DELETE_INPUT = z.object({
|
|
214
|
+
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
215
|
+
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
216
|
+
inputVariable: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe('Variable name (without "$") for the delete input. Defaults to "input". Declared as $<name>: RecordDeleteInput! — the schema-wide delete input carrying the record Id, not an <Object>-specific type.'),
|
|
220
|
+
operationName: graphqlName("Override the GraphQL operation name. Defaults to Delete<Object>.").optional(),
|
|
221
|
+
});
|
|
222
|
+
// --- sf_gql_connect ---------------------------------------------------------
|
|
223
|
+
export const CONNECT_INPUT = z.object({
|
|
224
|
+
org: orgAlias("Org alias or username resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
225
|
+
forceRefresh: z
|
|
226
|
+
.boolean()
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Re-download the schema even if it is already cached, clearing the on-disk introspection JSON, the in-memory parsed schema, and the ObjectInfo cache. Use after deploying new metadata (fields, picklist values, objects) so subsequent tools see the changes. Defaults to false."),
|
|
229
|
+
});
|
|
230
|
+
//# sourceMappingURL=input-schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-schemas.js","sourceRoot":"","sources":["../../src/schemas/input-schemas.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from "url";
|
|
|
12
12
|
import { Command } from "commander";
|
|
13
13
|
import { connectCommand } from "./commands/connect.js";
|
|
14
14
|
import { describeCommand } from "./commands/describe.js";
|
|
15
|
+
import { MIRRORS } from "./commands/mcp-mirror/commands.js";
|
|
15
16
|
import {
|
|
16
17
|
queryNew,
|
|
17
18
|
queryHelp,
|
|
@@ -761,4 +762,25 @@ program
|
|
|
761
762
|
}
|
|
762
763
|
});
|
|
763
764
|
|
|
764
|
-
|
|
765
|
+
// ── MCP-mirror commands ──────────────────────────────────────────────────────
|
|
766
|
+
//
|
|
767
|
+
// One top-level subcommand per MCP tool, generated from the shared MIRRORS
|
|
768
|
+
// table. Each takes a JSON blob (positional or stdin) and emits one JSON line on
|
|
769
|
+
// stdout — the same args/output the matching `sf_gql_*` MCP tool produces, over
|
|
770
|
+
// a different transport. Errors are emitted as a JSON envelope by `runMirror`,
|
|
771
|
+
// which sets `process.exitCode = 1` itself, so the actions just await (no
|
|
772
|
+
// try/catch, no `process.exit` that would truncate the buffered stdout write).
|
|
773
|
+
|
|
774
|
+
for (const mirror of MIRRORS) {
|
|
775
|
+
program
|
|
776
|
+
.command(mirror.name)
|
|
777
|
+
.description(`Mirror of the ${mirror.name.replace(/-/g, "_")} MCP tool. ${mirror.summary}`)
|
|
778
|
+
.argument("[json]", "JSON args; reads stdin when omitted")
|
|
779
|
+
.action(async (json?: string) => {
|
|
780
|
+
await mirror.run(json);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// `parseAsync` (not `parse`) so the async mirror actions are awaited before the
|
|
785
|
+
// process settles — otherwise their stdout write could race the event-loop exit.
|
|
786
|
+
await program.parseAsync();
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/* eslint-disable import/order -- the adapter imports must follow the vi.mock()
|
|
7
|
+
block so the intent builders are mocked before the adapters that import them
|
|
8
|
+
are loaded; import/order can't model that hoisting constraint. */
|
|
9
|
+
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { captureStdout } from "../../../__tests__/helpers/stdout.js";
|
|
12
|
+
|
|
13
|
+
// Mock every intent builder so the adapters can be exercised without auth,
|
|
14
|
+
// network, or a primed schema cache. Each returns a sentinel tagged with the
|
|
15
|
+
// builder name so we can assert the adapter dispatched to the right one.
|
|
16
|
+
vi.mock("../../../intent/build-list.js", () => ({
|
|
17
|
+
buildList: vi.fn(async (spec) => ({ from: "list", spec })),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../../../intent/build-detail.js", () => ({
|
|
20
|
+
buildDetail: vi.fn(async (spec) => ({ from: "detail", spec })),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("../../../intent/build-discover.js", () => ({
|
|
23
|
+
buildDiscover: vi.fn(async (spec) => ({ from: "discover", spec })),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../../intent/build-aggregate.js", () => ({
|
|
26
|
+
buildAggregate: vi.fn(async (spec) => ({ from: "aggregate", spec })),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../../../intent/build-raw.js", () => ({
|
|
29
|
+
buildRaw: vi.fn(async (spec) => ({ from: "raw", spec })),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../../../intent/build-create.js", () => ({
|
|
32
|
+
buildCreate: vi.fn(async (spec) => ({ from: "create", spec })),
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("../../../intent/build-update.js", () => ({
|
|
35
|
+
buildUpdate: vi.fn(async (spec) => ({ from: "update", spec })),
|
|
36
|
+
}));
|
|
37
|
+
vi.mock("../../../intent/build-delete.js", () => ({
|
|
38
|
+
buildDelete: vi.fn(async (spec) => ({ from: "delete", spec })),
|
|
39
|
+
}));
|
|
40
|
+
vi.mock("../../../intent/build-connect.js", () => ({
|
|
41
|
+
buildConnect: vi.fn(async (spec) => ({ from: "connect", spec })),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
import { MIRRORS } from "../commands.js";
|
|
45
|
+
|
|
46
|
+
interface Case {
|
|
47
|
+
name: string;
|
|
48
|
+
run: (json?: string) => Promise<void>;
|
|
49
|
+
validJson: string;
|
|
50
|
+
expectedFrom: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Per-tool fixtures: a representative valid payload and the sentinel `from` tag
|
|
54
|
+
// its mocked builder returns (the mock tags by short name, e.g. "list").
|
|
55
|
+
const FIXTURES: Record<string, { validJson: string; expectedFrom: string }> = {
|
|
56
|
+
"sf-gql-list": {
|
|
57
|
+
validJson: '{"org":"o","object":"Account","fields":["Id"]}',
|
|
58
|
+
expectedFrom: "list",
|
|
59
|
+
},
|
|
60
|
+
"sf-gql-detail": {
|
|
61
|
+
validJson: '{"org":"o","object":"Account","fields":["Id"]}',
|
|
62
|
+
expectedFrom: "detail",
|
|
63
|
+
},
|
|
64
|
+
"sf-gql-discover": { validJson: '{"org":"o","mode":"list_objects"}', expectedFrom: "discover" },
|
|
65
|
+
"sf-gql-aggregate": {
|
|
66
|
+
validJson: '{"org":"o","object":"Case","groupBy":["Status"]}',
|
|
67
|
+
expectedFrom: "aggregate",
|
|
68
|
+
},
|
|
69
|
+
"sf-gql-raw": {
|
|
70
|
+
validJson: '{"org":"o","commands":["select uiapi/query/Case/edges/node/Id/value"]}',
|
|
71
|
+
expectedFrom: "raw",
|
|
72
|
+
},
|
|
73
|
+
"sf-gql-create": { validJson: '{"org":"o","object":"Account"}', expectedFrom: "create" },
|
|
74
|
+
"sf-gql-update": { validJson: '{"org":"o","object":"Account"}', expectedFrom: "update" },
|
|
75
|
+
"sf-gql-delete": { validJson: '{"org":"o","object":"Account"}', expectedFrom: "delete" },
|
|
76
|
+
"sf-gql-connect": { validJson: '{"org":"o"}', expectedFrom: "connect" },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Drive the suite off MIRRORS so a new tool is covered automatically once it has
|
|
80
|
+
// a fixture; a missing fixture fails loudly rather than silently skipping.
|
|
81
|
+
const CASES: Case[] = MIRRORS.map((m) => {
|
|
82
|
+
const fixture = FIXTURES[m.name];
|
|
83
|
+
if (!fixture) throw new Error(`No test fixture for mirror command "${m.name}"`);
|
|
84
|
+
return { name: m.name, run: m.run, ...fixture };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// `sf-gql-detail`'s adapter, looked up from the table for the .strict() test.
|
|
88
|
+
const sfGqlDetail = MIRRORS.find((m) => m.name === "sf-gql-detail")!.run;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
process.exitCode = 0;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("mcp-mirror command adapters", () => {
|
|
95
|
+
it.each(CASES)(
|
|
96
|
+
"$name dispatches to its builder and prints the result as JSON",
|
|
97
|
+
async ({ run, validJson, expectedFrom }) => {
|
|
98
|
+
const out = await captureStdout(() => run(validJson));
|
|
99
|
+
|
|
100
|
+
const parsed = JSON.parse(out);
|
|
101
|
+
expect(parsed.from).toBe(expectedFrom);
|
|
102
|
+
// The adapter must hand the builder the *validated* input, not the raw
|
|
103
|
+
// string or a fixed object. The mocks echo `spec`, so this catches a
|
|
104
|
+
// payload-handoff regression (e.g. passing raw json instead of
|
|
105
|
+
// result.data) that the `from` tag alone would miss.
|
|
106
|
+
expect(parsed.spec).toEqual(JSON.parse(validJson));
|
|
107
|
+
expect(process.exitCode).toBe(0);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
it.each(CASES)("$name emits INVALID_ARGS on a bad arg", async ({ run }) => {
|
|
112
|
+
const out = await captureStdout(() => run("{not json"));
|
|
113
|
+
|
|
114
|
+
expect(JSON.parse(out).error.code).toBe("INVALID_ARGS");
|
|
115
|
+
expect(process.exitCode).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it.each(CASES)(
|
|
119
|
+
"$name emits INVALID_ARGS on a well-formed object that fails schema validation",
|
|
120
|
+
async ({ run }) => {
|
|
121
|
+
// A valid JSON object missing required fields must route through the
|
|
122
|
+
// Zod-failure branch (not just the JSON.parse branch). `{}` is missing
|
|
123
|
+
// `org` for every tool.
|
|
124
|
+
const out = await captureStdout(() => run("{}"));
|
|
125
|
+
|
|
126
|
+
const parsed = JSON.parse(out);
|
|
127
|
+
expect(parsed.error.code).toBe("INVALID_ARGS");
|
|
128
|
+
expect(parsed.error.details).toBeTruthy();
|
|
129
|
+
expect(process.exitCode).toBe(1);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
it("sf-gql-detail preserves DETAIL_INPUT's .strict() rejection of unknown top-level keys", async () => {
|
|
134
|
+
// DETAIL_INPUT is the one schema using .strict() (vs the .shape tools).
|
|
135
|
+
// `filter` is a sibling-tool param that detail intentionally rejects, so a
|
|
136
|
+
// hallucinating caller gets INVALID_ARGS rather than silent omission. This
|
|
137
|
+
// guards that the strict behavior survives the CLI adapter.
|
|
138
|
+
const out = await captureStdout(() =>
|
|
139
|
+
sfGqlDetail('{"org":"o","object":"Account","fields":["Id"],"filter":{}}'),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const parsed = JSON.parse(out);
|
|
143
|
+
expect(parsed.error.code).toBe("INVALID_ARGS");
|
|
144
|
+
expect(process.exitCode).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
});
|