@salesforce/graphiti 10.15.1 → 10.17.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 (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cli.js +21 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/mcp-mirror/commands.d.ts +16 -0
  5. package/dist/commands/mcp-mirror/commands.js +38 -0
  6. package/dist/commands/mcp-mirror/commands.js.map +1 -0
  7. package/dist/commands/mcp-mirror/run-mirror.d.ts +42 -0
  8. package/dist/commands/mcp-mirror/run-mirror.js +105 -0
  9. package/dist/commands/mcp-mirror/run-mirror.js.map +1 -0
  10. package/dist/lib/graphql-name.d.ts +2 -2
  11. package/dist/lib/graphql-name.js +2 -2
  12. package/dist/mcp/stdio.js +0 -0
  13. package/dist/mcp/tools/sf-gql-aggregate.js +2 -55
  14. package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -1
  15. package/dist/mcp/tools/sf-gql-connect.js +2 -9
  16. package/dist/mcp/tools/sf-gql-connect.js.map +1 -1
  17. package/dist/mcp/tools/sf-gql-create.js +2 -15
  18. package/dist/mcp/tools/sf-gql-create.js.map +1 -1
  19. package/dist/mcp/tools/sf-gql-delete.js +2 -11
  20. package/dist/mcp/tools/sf-gql-delete.js.map +1 -1
  21. package/dist/mcp/tools/sf-gql-detail.js +2 -30
  22. package/dist/mcp/tools/sf-gql-detail.js.map +1 -1
  23. package/dist/mcp/tools/sf-gql-discover.js +2 -33
  24. package/dist/mcp/tools/sf-gql-discover.js.map +1 -1
  25. package/dist/mcp/tools/sf-gql-list.js +2 -32
  26. package/dist/mcp/tools/sf-gql-list.js.map +1 -1
  27. package/dist/mcp/tools/sf-gql-raw.js +2 -19
  28. package/dist/mcp/tools/sf-gql-raw.js.map +1 -1
  29. package/dist/mcp/tools/sf-gql-update.js +2 -15
  30. package/dist/mcp/tools/sf-gql-update.js.map +1 -1
  31. package/dist/{mcp/tools/shared/zod-schemas.js → schemas/fields.js} +2 -2
  32. package/dist/schemas/fields.js.map +1 -0
  33. package/dist/schemas/input-schemas.d.ts +318 -0
  34. package/dist/schemas/input-schemas.js +230 -0
  35. package/dist/schemas/input-schemas.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +23 -1
  38. package/src/commands/mcp-mirror/__tests__/commands.spec.ts +146 -0
  39. package/src/commands/mcp-mirror/__tests__/run-mirror.spec.ts +251 -0
  40. package/src/commands/mcp-mirror/commands.ts +92 -0
  41. package/src/commands/mcp-mirror/run-mirror.ts +149 -0
  42. package/src/lib/graphql-name.ts +2 -2
  43. package/src/mcp/tools/sf-gql-aggregate.ts +2 -68
  44. package/src/mcp/tools/sf-gql-connect.ts +2 -11
  45. package/src/mcp/tools/sf-gql-create.ts +2 -21
  46. package/src/mcp/tools/sf-gql-delete.ts +2 -15
  47. package/src/mcp/tools/sf-gql-detail.ts +2 -42
  48. package/src/mcp/tools/sf-gql-discover.ts +2 -36
  49. package/src/mcp/tools/sf-gql-list.ts +2 -41
  50. package/src/mcp/tools/sf-gql-raw.ts +2 -25
  51. package/src/mcp/tools/sf-gql-update.ts +2 -21
  52. package/src/{mcp/tools/shared/zod-schemas.ts → schemas/fields.ts} +1 -1
  53. package/src/schemas/input-schemas.ts +305 -0
  54. package/dist/mcp/tools/shared/zod-schemas.js.map +0 -1
  55. package/dist/{mcp/tools/shared/zod-schemas.d.ts → schemas/fields.d.ts} +2 -2
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { beforeEach, describe, expect, it } from "vitest";
8
+ import { z } from "zod";
9
+ import { captureStdout } from "../../../__tests__/helpers/stdout.js";
10
+ import { SchemaRefreshError } from "../../../lib/prime-schema.js";
11
+ import { runMirror } from "../run-mirror.js";
12
+
13
+ const SCHEMA = z.object({ org: z.string().min(1), object: z.string() });
14
+
15
+ beforeEach(() => {
16
+ // runMirror signals failure via process.exitCode; establish a known 0
17
+ // baseline so "success leaves it 0" is deterministic and a prior failing
18
+ // case doesn't leak into the next.
19
+ process.exitCode = 0;
20
+ });
21
+
22
+ describe("runMirror", () => {
23
+ it("prints the build output as one JSON line on success and leaves exitCode 0", async () => {
24
+ const build = async (input: { org: string; object: string }) => ({
25
+ query: `# ${input.object}`,
26
+ echoedOrg: input.org,
27
+ });
28
+
29
+ const out = await captureStdout(() =>
30
+ runMirror('{"org":"ebikes","object":"Account"}', SCHEMA, build),
31
+ );
32
+
33
+ expect(out.trim().split("\n")).toHaveLength(1);
34
+ expect(JSON.parse(out)).toEqual({ query: "# Account", echoedOrg: "ebikes" });
35
+ expect(process.exitCode).toBe(0);
36
+ });
37
+
38
+ it("passes the parsed+validated input to the build function", async () => {
39
+ let received: unknown;
40
+ const build = async (input: unknown) => {
41
+ received = input;
42
+ return {};
43
+ };
44
+
45
+ await captureStdout(() => runMirror('{"org":"o","object":"Account"}', SCHEMA, build));
46
+
47
+ expect(received).toEqual({ org: "o", object: "Account" });
48
+ });
49
+
50
+ it("emits an INVALID_ARGS envelope and sets exitCode 1 on malformed JSON", async () => {
51
+ const build = async () => ({});
52
+
53
+ const out = await captureStdout(() => runMirror("{not json", SCHEMA, build));
54
+
55
+ const parsed = JSON.parse(out);
56
+ expect(parsed.error.code).toBe("INVALID_ARGS");
57
+ expect(parsed.error.message).toBeTruthy();
58
+ expect(process.exitCode).toBe(1);
59
+ });
60
+
61
+ it("emits an INVALID_ARGS envelope with Zod details on schema-validation failure", async () => {
62
+ const build = async () => ({});
63
+
64
+ // org is "" → fails min(1); object missing
65
+ const out = await captureStdout(() => runMirror('{"org":""}', SCHEMA, build));
66
+
67
+ const parsed = JSON.parse(out);
68
+ expect(parsed.error.code).toBe("INVALID_ARGS");
69
+ expect(parsed.error.details).toBeTruthy();
70
+ expect(process.exitCode).toBe(1);
71
+ });
72
+
73
+ it("does not call build when validation fails", async () => {
74
+ let called = false;
75
+ const build = async () => {
76
+ called = true;
77
+ return {};
78
+ };
79
+
80
+ await captureStdout(() => runMirror('{"org":""}', SCHEMA, build));
81
+
82
+ expect(called).toBe(false);
83
+ });
84
+
85
+ it("reads JSON from stdin when no positional arg is given", async () => {
86
+ const build = async (input: { org: string; object: string }) => ({ org: input.org });
87
+
88
+ const out = await captureStdout(() =>
89
+ runMirror(undefined, SCHEMA, build, {
90
+ readStdin: async () => '{"org":"from-stdin","object":"Case"}',
91
+ }),
92
+ );
93
+
94
+ expect(JSON.parse(out)).toEqual({ org: "from-stdin" });
95
+ expect(process.exitCode).toBe(0);
96
+ });
97
+
98
+ it("emits INVALID_ARGS without hanging when no arg is given and stdin is a TTY", async () => {
99
+ let stdinRead = false;
100
+ const build = async () => ({});
101
+
102
+ const out = await captureStdout(() =>
103
+ runMirror(undefined, SCHEMA, build, {
104
+ isTTY: true,
105
+ readStdin: async () => {
106
+ stdinRead = true;
107
+ return "";
108
+ },
109
+ }),
110
+ );
111
+
112
+ expect(stdinRead).toBe(false); // guarded before touching stdin
113
+ expect(JSON.parse(out).error.code).toBe("INVALID_ARGS");
114
+ expect(process.exitCode).toBe(1);
115
+ });
116
+
117
+ it("maps SchemaRefreshError to SCHEMA_PRIME_FAILED", async () => {
118
+ const build = async () => {
119
+ throw new SchemaRefreshError("schema is stale", {
120
+ instanceUrl: "https://x.my.salesforce.com",
121
+ });
122
+ };
123
+
124
+ const out = await captureStdout(() =>
125
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
126
+ );
127
+
128
+ const parsed = JSON.parse(out);
129
+ expect(parsed.error.code).toBe("SCHEMA_PRIME_FAILED");
130
+ expect(parsed.error.message).toContain("stale");
131
+ expect(process.exitCode).toBe(1);
132
+ });
133
+
134
+ it("maps an unrecognized thrown error to INTERNAL and preserves its message", async () => {
135
+ const build = async () => {
136
+ throw new Error("something totally unexpected happened");
137
+ };
138
+
139
+ const out = await captureStdout(() =>
140
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
141
+ );
142
+
143
+ const parsed = JSON.parse(out);
144
+ expect(parsed.error.code).toBe("INTERNAL");
145
+ expect(parsed.error.message).toContain("something totally unexpected");
146
+ expect(process.exitCode).toBe(1);
147
+ });
148
+
149
+ it("best-effort classifies auth-shaped messages as AUTH_FAILED", async () => {
150
+ const build = async () => {
151
+ throw new Error("Authentication failed; no valid token for org");
152
+ };
153
+
154
+ const out = await captureStdout(() =>
155
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
156
+ );
157
+
158
+ const parsed = JSON.parse(out);
159
+ expect(parsed.error.code).toBe("AUTH_FAILED");
160
+ expect(parsed.error.message).toContain("Authentication failed");
161
+ expect(process.exitCode).toBe(1);
162
+ });
163
+
164
+ it("best-effort classifies a ~/.sf path message as AUTH_FAILED", async () => {
165
+ const build = async () => {
166
+ throw new Error("Could not read credentials from ~/.sf");
167
+ };
168
+
169
+ const out = await captureStdout(() =>
170
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
171
+ );
172
+
173
+ expect(JSON.parse(out).error.code).toBe("AUTH_FAILED");
174
+ });
175
+
176
+ it("best-effort classifies introspection/priming messages as SCHEMA_PRIME_FAILED", async () => {
177
+ const build = async () => {
178
+ throw new Error("schema download failed during introspection");
179
+ };
180
+
181
+ const out = await captureStdout(() =>
182
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
183
+ );
184
+
185
+ expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED");
186
+ });
187
+
188
+ it("still prefers the typed SchemaRefreshError signal over message regex", async () => {
189
+ // A SchemaRefreshError whose message happens to contain an auth-ish word
190
+ // must still classify by type, not by the regex.
191
+ const build = async () => {
192
+ throw new SchemaRefreshError("stale; re-authorization may help", {
193
+ instanceUrl: "https://x.my.salesforce.com",
194
+ });
195
+ };
196
+
197
+ const out = await captureStdout(() =>
198
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
199
+ );
200
+
201
+ expect(JSON.parse(out).error.code).toBe("SCHEMA_PRIME_FAILED");
202
+ });
203
+
204
+ it("treats a literal '-' positional arg as 'read from stdin'", async () => {
205
+ const build = async (input: { org: string; object: string }) => ({ org: input.org });
206
+
207
+ const out = await captureStdout(() =>
208
+ runMirror("-", SCHEMA, build, {
209
+ readStdin: async () => '{"org":"via-dash","object":"Case"}',
210
+ }),
211
+ );
212
+
213
+ expect(JSON.parse(out)).toEqual({ org: "via-dash" });
214
+ expect(process.exitCode).toBe(0);
215
+ });
216
+
217
+ it("does not leak a stack trace in the INTERNAL envelope by default", async () => {
218
+ const build = async () => {
219
+ throw new Error("boom");
220
+ };
221
+
222
+ const out = await captureStdout(() =>
223
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
224
+ );
225
+
226
+ const parsed = JSON.parse(out);
227
+ expect(parsed.error.code).toBe("INTERNAL");
228
+ expect(parsed.error.details).toBeUndefined();
229
+ });
230
+
231
+ it("attaches the stack to INTERNAL details when GRAPHITI_DEBUG=1", async () => {
232
+ const prev = process.env.GRAPHITI_DEBUG;
233
+ process.env.GRAPHITI_DEBUG = "1";
234
+ try {
235
+ const build = async () => {
236
+ throw new Error("boom");
237
+ };
238
+
239
+ const out = await captureStdout(() =>
240
+ runMirror('{"org":"o","object":"Account"}', SCHEMA, build),
241
+ );
242
+
243
+ const parsed = JSON.parse(out);
244
+ expect(parsed.error.code).toBe("INTERNAL");
245
+ expect(parsed.error.details.stack).toContain("boom");
246
+ } finally {
247
+ if (prev === undefined) delete process.env.GRAPHITI_DEBUG;
248
+ else process.env.GRAPHITI_DEBUG = prev;
249
+ }
250
+ });
251
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * CLI mirror commands — one entry per MCP tool. Each binds a Zod input schema
9
+ * to the matching `intent/build*()` function; all the real logic lives in
10
+ * `runMirror` (`run-mirror.ts`). The commands are siblings of the MCP tools: same
11
+ * args, same output, different transport.
12
+ *
13
+ * The bindings are data, not bespoke functions — `cli.ts` registers them in a
14
+ * loop and the tests iterate them, so adding a tool is one `defineMirror(...)`
15
+ * line here.
16
+ */
17
+
18
+ import { type z } from "zod";
19
+ import { runMirror } from "./run-mirror.js";
20
+ import { buildAggregate } from "../../intent/build-aggregate.js";
21
+ import { buildConnect } from "../../intent/build-connect.js";
22
+ import { buildCreate } from "../../intent/build-create.js";
23
+ import { buildDelete } from "../../intent/build-delete.js";
24
+ import { buildDetail } from "../../intent/build-detail.js";
25
+ import { buildDiscover } from "../../intent/build-discover.js";
26
+ import { buildList } from "../../intent/build-list.js";
27
+ import { buildRaw } from "../../intent/build-raw.js";
28
+ import { buildUpdate } from "../../intent/build-update.js";
29
+ import {
30
+ AGGREGATE_INPUT,
31
+ CONNECT_INPUT,
32
+ CREATE_INPUT,
33
+ DELETE_INPUT,
34
+ DETAIL_INPUT,
35
+ DISCOVER_INPUT,
36
+ LIST_INPUT,
37
+ RAW_INPUT,
38
+ UPDATE_INPUT,
39
+ } from "../../schemas/input-schemas.js";
40
+
41
+ /** A single CLI mirror command: its subcommand name, help summary, and runner. */
42
+ export interface MirrorCommand {
43
+ /** CLI subcommand name, e.g. "sf-gql-list", matching the MCP tool's `sf_gql_*`. */
44
+ name: string;
45
+ /** One-line `--help` summary. */
46
+ summary: string;
47
+ /** Read JSON (positional arg or stdin), validate, build, emit one JSON line. */
48
+ run: (jsonArg?: string) => Promise<void>;
49
+ }
50
+
51
+ /**
52
+ * Bind a schema to its builder behind a uniform `run`. The generic `T` is
53
+ * captured here, at construction, where the schema and builder types still line
54
+ * up — so {@link MIRRORS} can be a single homogeneous array without forcing the
55
+ * builders through an `unknown` parameter (which would fail by contravariance).
56
+ */
57
+ function defineMirror<T>(
58
+ name: string,
59
+ summary: string,
60
+ schema: z.ZodType<T>,
61
+ build: (input: T) => Promise<unknown>,
62
+ ): MirrorCommand {
63
+ return { name, summary, run: (jsonArg) => runMirror(jsonArg, schema, build) };
64
+ }
65
+
66
+ /** Every CLI mirror command. Single source of truth for `cli.ts` and the tests. */
67
+ export const MIRRORS: MirrorCommand[] = [
68
+ defineMirror("sf-gql-list", "Build a UIAPI list query.", LIST_INPUT, buildList),
69
+ defineMirror("sf-gql-detail", "Build a single-record detail query.", DETAIL_INPUT, buildDetail),
70
+ defineMirror("sf-gql-discover", "Discover UIAPI schema metadata.", DISCOVER_INPUT, buildDiscover),
71
+ defineMirror(
72
+ "sf-gql-aggregate",
73
+ "Build a UIAPI aggregate query.",
74
+ AGGREGATE_INPUT,
75
+ buildAggregate,
76
+ ),
77
+ defineMirror(
78
+ "sf-gql-raw",
79
+ "Build an arbitrary query from CLI-style commands.",
80
+ RAW_INPUT,
81
+ buildRaw,
82
+ ),
83
+ defineMirror("sf-gql-create", "Build a UIAPI create mutation.", CREATE_INPUT, buildCreate),
84
+ defineMirror("sf-gql-update", "Build a UIAPI update mutation.", UPDATE_INPUT, buildUpdate),
85
+ defineMirror("sf-gql-delete", "Build a UIAPI delete mutation.", DELETE_INPUT, buildDelete),
86
+ defineMirror(
87
+ "sf-gql-connect",
88
+ "Prime/refresh an org's schema cache.",
89
+ CONNECT_INPUT,
90
+ buildConnect,
91
+ ),
92
+ ];
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { type z } from "zod";
8
+ import { SchemaRefreshError } from "../../lib/prime-schema.js";
9
+
10
+ /**
11
+ * Shared adapter for the `sf-gql-*` CLI mirror commands. Each command reads a
12
+ * JSON blob (positional arg or stdin), validates it against the same Zod schema
13
+ * the matching MCP tool advertises, calls the same `intent/build*()` function
14
+ * the MCP tool calls, and emits exactly one JSON line on stdout. This keeps the
15
+ * CLI a sibling adapter to the MCP server — same args, same output, different
16
+ * transport.
17
+ *
18
+ * Failures emit a JSON error envelope and set `process.exitCode = 1`. We use
19
+ * `process.exitCode` rather than `process.exit()` so the buffered stdout write
20
+ * has time to flush before the process ends.
21
+ */
22
+
23
+ /** Error codes surfaced in the failure envelope. */
24
+ export type MirrorErrorCode = "INVALID_ARGS" | "AUTH_FAILED" | "SCHEMA_PRIME_FAILED" | "INTERNAL";
25
+
26
+ /**
27
+ * Best-effort classification of a plain `Error` message into a more specific
28
+ * code. The intent/auth layers throw plain `Error`s with no type signal, so the
29
+ * only available hint is the message text. This is a NON-CONTRACT heuristic: a
30
+ * reworded upstream message can fall through to `INTERNAL`. The verbatim message
31
+ * is always preserved either way, and the typed `SchemaRefreshError` signal
32
+ * (checked before this) takes precedence. Patterns mirror the MCP-CLI prior art.
33
+ */
34
+ function classifyErrorMessage(message: string): MirrorErrorCode {
35
+ if (
36
+ /\bauth(?:entication|orization)?\b/i.test(message) ||
37
+ /\bno org\b/i.test(message) ||
38
+ message.includes("~/.sf")
39
+ ) {
40
+ return "AUTH_FAILED";
41
+ }
42
+ if (/\b(?:introspect(?:ion)?|priming|schema download)\b/i.test(message)) {
43
+ return "SCHEMA_PRIME_FAILED";
44
+ }
45
+ return "INTERNAL";
46
+ }
47
+
48
+ export interface MirrorErrorEnvelope {
49
+ error: {
50
+ code: MirrorErrorCode;
51
+ message: string;
52
+ details?: unknown;
53
+ };
54
+ }
55
+
56
+ export interface RunMirrorDeps {
57
+ /** Read the full JSON payload from stdin. Injectable for tests. */
58
+ readStdin?: () => Promise<string>;
59
+ /** Whether stdin is a TTY. When true and no arg is given, we don't block on stdin. */
60
+ isTTY?: boolean;
61
+ }
62
+
63
+ async function readAllStdin(): Promise<string> {
64
+ const chunks: Buffer[] = [];
65
+ for await (const chunk of process.stdin) {
66
+ chunks.push(chunk as Buffer);
67
+ }
68
+ return Buffer.concat(chunks).toString("utf8");
69
+ }
70
+
71
+ function emitError(code: MirrorErrorCode, message: string, details?: unknown): void {
72
+ const envelope: MirrorErrorEnvelope = {
73
+ error: details === undefined ? { code, message } : { code, message, details },
74
+ };
75
+ console.log(JSON.stringify(envelope));
76
+ // Not process.exit(1): let the buffered stdout write flush first.
77
+ process.exitCode = 1;
78
+ }
79
+
80
+ /**
81
+ * Run one mirror command end-to-end.
82
+ *
83
+ * @param jsonArg Positional JSON argument, or undefined to read from stdin.
84
+ * @param schema The tool's Zod input schema (shared with the MCP tool).
85
+ * @param build The intent-layer builder; receives the validated input.
86
+ * @param deps Injectable stdin reader / TTY flag for testing.
87
+ */
88
+ export async function runMirror<T>(
89
+ jsonArg: string | undefined,
90
+ schema: z.ZodType<T>,
91
+ build: (input: T) => Promise<unknown>,
92
+ deps: RunMirrorDeps = {},
93
+ ): Promise<void> {
94
+ // 1. Source the raw JSON: a positional arg wins, except a literal "-" which
95
+ // is the conventional "read from stdin" sentinel. Otherwise read stdin
96
+ // (unless TTY, where blocking would hang waiting for a human to type).
97
+ const isTTY = deps.isTTY ?? process.stdin.isTTY;
98
+ const wantsStdin = jsonArg === undefined || jsonArg === "-";
99
+ let raw: string;
100
+ if (!wantsStdin) {
101
+ raw = jsonArg as string;
102
+ } else if (isTTY) {
103
+ emitError("INVALID_ARGS", "No input provided. Pass a JSON argument or pipe JSON to stdin.");
104
+ return;
105
+ } else {
106
+ const readStdin = deps.readStdin ?? readAllStdin;
107
+ raw = await readStdin();
108
+ if (!raw.trim()) {
109
+ emitError("INVALID_ARGS", "No input provided. Pass a JSON argument or pipe JSON to stdin.");
110
+ return;
111
+ }
112
+ }
113
+
114
+ // 2. Parse JSON.
115
+ let json: unknown;
116
+ try {
117
+ json = JSON.parse(raw);
118
+ } catch (err) {
119
+ emitError("INVALID_ARGS", `Invalid JSON: ${(err as Error).message}`);
120
+ return;
121
+ }
122
+
123
+ // 3. Validate against the shared Zod schema.
124
+ const result = schema.safeParse(json);
125
+ if (!result.success) {
126
+ emitError("INVALID_ARGS", "Input failed schema validation.", result.error.issues);
127
+ return;
128
+ }
129
+
130
+ // 4. Build and emit. Classify failures: the typed SchemaRefreshError signal
131
+ // first, then a best-effort message regex (AUTH_FAILED / SCHEMA_PRIME_FAILED),
132
+ // else INTERNAL. The verbatim message is always preserved.
133
+ try {
134
+ const output = await build(result.data);
135
+ console.log(JSON.stringify(output));
136
+ } catch (err) {
137
+ const message = err instanceof Error ? err.message : String(err);
138
+ // Typed signal wins; otherwise fall back to the best-effort message regex.
139
+ const code: MirrorErrorCode =
140
+ err instanceof SchemaRefreshError ? "SCHEMA_PRIME_FAILED" : classifyErrorMessage(message);
141
+ // The stack is omitted by default so the envelope never leaks internals;
142
+ // GRAPHITI_DEBUG=1 opts into attaching it under `details` for debugging.
143
+ const details =
144
+ process.env.GRAPHITI_DEBUG === "1" && err instanceof Error && err.stack
145
+ ? { stack: err.stack }
146
+ : undefined;
147
+ emitError(code, message, details);
148
+ }
149
+ }
@@ -14,8 +14,8 @@
14
14
  * warning (`buildOutput` never throws) while the malformed query is still
15
15
  * returned as `ToolOutput.query`.
16
16
  *
17
- * The MCP boundary has its own zod-based `graphqlName()` factory
18
- * (`src/mcp/tools/shared/zod-schemas.ts`); this is the intent-layer twin used
17
+ * The schema boundary has its own zod-based `graphqlName()` factory
18
+ * (`src/schemas/fields.ts`); this is the intent-layer twin used
19
19
  * by the builders, which are also reachable directly (CLI, eval harness).
20
20
  */
21
21
  export const GRAPHQL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -5,81 +5,15 @@
5
5
  */
6
6
 
7
7
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
9
- import { graphqlName } from "./shared/zod-schemas.js";
10
8
  import { buildAggregate } from "../../intent/build-aggregate.js";
11
- import { GROUP_BY_FUNCTIONS } from "../../intent/types.js";
12
9
  import { type PrimeDeps } from "../../lib/prime-schema.js";
10
+ import { AGGREGATE_INPUT } from "../../schemas/input-schemas.js";
13
11
 
14
12
  export interface SfGqlAggregateToolOptions {
15
13
  primeDeps?: PrimeDeps;
16
14
  }
17
15
 
18
- const aliasField = z
19
- .string()
20
- .optional()
21
- .describe("GraphQL alias for this aggregation's result key.");
22
-
23
- const aggregationSchema = z.discriminatedUnion("function", [
24
- z.object({
25
- function: z.enum(["count", "countDistinct"]),
26
- field: z.string().optional().describe('SObject field API name. Defaults to "Id" when omitted.'),
27
- alias: aliasField,
28
- }),
29
- z.object({
30
- function: z.enum(["sum", "avg", "min", "max"]),
31
- field: z.string().describe("SObject field API name. Required for sum/avg/min/max."),
32
- alias: aliasField,
33
- }),
34
- ]);
35
-
36
- const groupByElementSchema = z.union([
37
- z.string(),
38
- z.object({
39
- field: z.string().describe("SObject field API name (DateTime/Date field)."),
40
- function: z
41
- .enum(GROUP_BY_FUNCTIONS)
42
- .describe("Date bucketing function from UIAPI GroupByFunction enum."),
43
- }),
44
- ]);
45
-
46
- const inputSchema = {
47
- org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
48
- object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
49
- groupBy: z
50
- .array(groupByElementSchema)
51
- .optional()
52
- .describe(
53
- "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).",
54
- ),
55
- aggregations: z
56
- .array(aggregationSchema)
57
- .optional()
58
- .describe(
59
- "List of aggregation functions to compute. Each entry projects under its alias (or default `<function><PascalCaseField>`).",
60
- ),
61
- filter: z
62
- .record(z.unknown())
63
- .optional()
64
- .describe(
65
- "<Object>_Filter applied to every aggregation. String leaves matching $varName promote to typed query variables.",
66
- ),
67
- orderBy: z
68
- .union([z.record(z.unknown()), z.array(z.record(z.unknown()))])
69
- .optional()
70
- .describe(
71
- "<Object>_OrderBy. Singleton object preferred; arrays are collapsed to the first entry. String leaves matching $varName promote to typed query variables.",
72
- ),
73
- first: z
74
- .number()
75
- .int()
76
- .positive()
77
- .optional()
78
- .describe("Connection page size (top-N pattern). No default — omit for all buckets."),
79
- operationName: graphqlName(
80
- "Override the GraphQL operation name. Defaults to <Object>Aggregate.",
81
- ).optional(),
82
- };
16
+ const inputSchema = AGGREGATE_INPUT.shape;
83
17
 
84
18
  export function registerSfGqlAggregateTool(
85
19
  server: McpServer,
@@ -5,21 +5,12 @@
5
5
  */
6
6
 
7
7
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
9
- import { orgAlias } from "./shared/zod-schemas.js";
10
8
  import { buildConnect, type ConnectDeps } from "../../intent/build-connect.js";
9
+ import { CONNECT_INPUT } from "../../schemas/input-schemas.js";
11
10
 
12
11
  export type SfGqlConnectToolOptions = ConnectDeps;
13
12
 
14
- const inputSchema = {
15
- org: orgAlias("Org alias or username resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
16
- forceRefresh: z
17
- .boolean()
18
- .optional()
19
- .describe(
20
- "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.",
21
- ),
22
- };
13
+ const inputSchema = CONNECT_INPUT.shape;
23
14
 
24
15
  export function registerSfGqlConnectTool(
25
16
  server: McpServer,
@@ -5,34 +5,15 @@
5
5
  */
6
6
 
7
7
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
9
- import { graphqlName } from "./shared/zod-schemas.js";
10
8
  import { buildCreate } from "../../intent/build-create.js";
11
9
  import { type PrimeDeps } from "../../lib/prime-schema.js";
10
+ import { CREATE_INPUT } from "../../schemas/input-schemas.js";
12
11
 
13
12
  export interface SfGqlCreateToolOptions {
14
13
  primeDeps?: PrimeDeps;
15
14
  }
16
15
 
17
- const inputSchema = {
18
- org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
19
- object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
20
- returnFields: z
21
- .array(z.string())
22
- .optional()
23
- .describe(
24
- '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.',
25
- ),
26
- inputVariable: z
27
- .string()
28
- .optional()
29
- .describe(
30
- 'Variable name (without "$") for the create input. Defaults to "input". Declared as $<name>: <Object>CreateInput!.',
31
- ),
32
- operationName: graphqlName(
33
- "Override the GraphQL operation name. Defaults to Create<Object>.",
34
- ).optional(),
35
- };
16
+ const inputSchema = CREATE_INPUT.shape;
36
17
 
37
18
  export function registerSfGqlCreateTool(
38
19
  server: McpServer,
@@ -5,28 +5,15 @@
5
5
  */
6
6
 
7
7
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
9
- import { graphqlName } from "./shared/zod-schemas.js";
10
8
  import { buildDelete } from "../../intent/build-delete.js";
11
9
  import { type PrimeDeps } from "../../lib/prime-schema.js";
10
+ import { DELETE_INPUT } from "../../schemas/input-schemas.js";
12
11
 
13
12
  export interface SfGqlDeleteToolOptions {
14
13
  primeDeps?: PrimeDeps;
15
14
  }
16
15
 
17
- const inputSchema = {
18
- org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
19
- object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
20
- inputVariable: z
21
- .string()
22
- .optional()
23
- .describe(
24
- '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.',
25
- ),
26
- operationName: graphqlName(
27
- "Override the GraphQL operation name. Defaults to Delete<Object>.",
28
- ).optional(),
29
- };
16
+ const inputSchema = DELETE_INPUT.shape;
30
17
 
31
18
  export function registerSfGqlDeleteTool(
32
19
  server: McpServer,