@salesforce/graphiti 10.10.2

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 (282) hide show
  1. package/AGENT_GUIDE.md +424 -0
  2. package/CHANGELOG.md +448 -0
  3. package/LICENSE.txt +82 -0
  4. package/README.md +204 -0
  5. package/TASK.md +249 -0
  6. package/dist/cli.d.ts +7 -0
  7. package/dist/cli.js +683 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/args.d.ts +13 -0
  10. package/dist/commands/args.js +207 -0
  11. package/dist/commands/args.js.map +1 -0
  12. package/dist/commands/build.d.ts +11 -0
  13. package/dist/commands/build.js +209 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/connect.d.ts +8 -0
  16. package/dist/commands/connect.js +55 -0
  17. package/dist/commands/connect.js.map +1 -0
  18. package/dist/commands/describe.d.ts +6 -0
  19. package/dist/commands/describe.js +229 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/meta.d.ts +9 -0
  22. package/dist/commands/meta.js +140 -0
  23. package/dist/commands/meta.js.map +1 -0
  24. package/dist/commands/navigate.d.ts +14 -0
  25. package/dist/commands/navigate.js +105 -0
  26. package/dist/commands/navigate.js.map +1 -0
  27. package/dist/commands/query-helpers.d.ts +80 -0
  28. package/dist/commands/query-helpers.js +865 -0
  29. package/dist/commands/query-helpers.js.map +1 -0
  30. package/dist/commands/query.d.ts +26 -0
  31. package/dist/commands/query.js +901 -0
  32. package/dist/commands/query.js.map +1 -0
  33. package/dist/commands/review.d.ts +18 -0
  34. package/dist/commands/review.js +533 -0
  35. package/dist/commands/review.js.map +1 -0
  36. package/dist/commands/session-mgmt.d.ts +25 -0
  37. package/dist/commands/session-mgmt.js +206 -0
  38. package/dist/commands/session-mgmt.js.map +1 -0
  39. package/dist/commands/type.d.ts +6 -0
  40. package/dist/commands/type.js +342 -0
  41. package/dist/commands/type.js.map +1 -0
  42. package/dist/commands/validate-input.d.ts +6 -0
  43. package/dist/commands/validate-input.js +32 -0
  44. package/dist/commands/validate-input.js.map +1 -0
  45. package/dist/intent/build-aggregate.d.ts +33 -0
  46. package/dist/intent/build-aggregate.js +134 -0
  47. package/dist/intent/build-aggregate.js.map +1 -0
  48. package/dist/intent/build-create.d.ts +14 -0
  49. package/dist/intent/build-create.js +16 -0
  50. package/dist/intent/build-create.js.map +1 -0
  51. package/dist/intent/build-delete.d.ts +30 -0
  52. package/dist/intent/build-delete.js +53 -0
  53. package/dist/intent/build-delete.js.map +1 -0
  54. package/dist/intent/build-detail.d.ts +32 -0
  55. package/dist/intent/build-detail.js +80 -0
  56. package/dist/intent/build-detail.js.map +1 -0
  57. package/dist/intent/build-discover.d.ts +30 -0
  58. package/dist/intent/build-discover.js +149 -0
  59. package/dist/intent/build-discover.js.map +1 -0
  60. package/dist/intent/build-list.d.ts +28 -0
  61. package/dist/intent/build-list.js +75 -0
  62. package/dist/intent/build-list.js.map +1 -0
  63. package/dist/intent/build-mutation.d.ts +23 -0
  64. package/dist/intent/build-mutation.js +54 -0
  65. package/dist/intent/build-mutation.js.map +1 -0
  66. package/dist/intent/build-output.d.ts +27 -0
  67. package/dist/intent/build-output.js +60 -0
  68. package/dist/intent/build-output.js.map +1 -0
  69. package/dist/intent/build-raw.d.ts +23 -0
  70. package/dist/intent/build-raw.js +54 -0
  71. package/dist/intent/build-raw.js.map +1 -0
  72. package/dist/intent/build-update.d.ts +14 -0
  73. package/dist/intent/build-update.js +16 -0
  74. package/dist/intent/build-update.js.map +1 -0
  75. package/dist/intent/get-schema-with-priming.d.ts +26 -0
  76. package/dist/intent/get-schema-with-priming.js +32 -0
  77. package/dist/intent/get-schema-with-priming.js.map +1 -0
  78. package/dist/intent/select-child-relationship.d.ts +19 -0
  79. package/dist/intent/select-child-relationship.js +38 -0
  80. package/dist/intent/select-child-relationship.js.map +1 -0
  81. package/dist/intent/types.d.ts +159 -0
  82. package/dist/intent/types.js +21 -0
  83. package/dist/intent/types.js.map +1 -0
  84. package/dist/lib/apply-command.d.ts +15 -0
  85. package/dist/lib/apply-command.js +238 -0
  86. package/dist/lib/apply-command.js.map +1 -0
  87. package/dist/lib/auth.d.ts +38 -0
  88. package/dist/lib/auth.js +113 -0
  89. package/dist/lib/auth.js.map +1 -0
  90. package/dist/lib/codegen.d.ts +32 -0
  91. package/dist/lib/codegen.js +700 -0
  92. package/dist/lib/codegen.js.map +1 -0
  93. package/dist/lib/command-registry.d.ts +59 -0
  94. package/dist/lib/command-registry.js +366 -0
  95. package/dist/lib/command-registry.js.map +1 -0
  96. package/dist/lib/formatter.d.ts +76 -0
  97. package/dist/lib/formatter.js +419 -0
  98. package/dist/lib/formatter.js.map +1 -0
  99. package/dist/lib/fs-utils.d.ts +23 -0
  100. package/dist/lib/fs-utils.js +46 -0
  101. package/dist/lib/fs-utils.js.map +1 -0
  102. package/dist/lib/graphql-name.d.ts +27 -0
  103. package/dist/lib/graphql-name.js +32 -0
  104. package/dist/lib/graphql-name.js.map +1 -0
  105. package/dist/lib/interactive.d.ts +6 -0
  106. package/dist/lib/interactive.js +562 -0
  107. package/dist/lib/interactive.js.map +1 -0
  108. package/dist/lib/introspect.d.ts +40 -0
  109. package/dist/lib/introspect.js +280 -0
  110. package/dist/lib/introspect.js.map +1 -0
  111. package/dist/lib/object-info.d.ts +79 -0
  112. package/dist/lib/object-info.js +313 -0
  113. package/dist/lib/object-info.js.map +1 -0
  114. package/dist/lib/path-selection.d.ts +50 -0
  115. package/dist/lib/path-selection.js +146 -0
  116. package/dist/lib/path-selection.js.map +1 -0
  117. package/dist/lib/prime-schema.d.ts +59 -0
  118. package/dist/lib/prime-schema.js +158 -0
  119. package/dist/lib/prime-schema.js.map +1 -0
  120. package/dist/lib/query-builder.d.ts +10 -0
  121. package/dist/lib/query-builder.js +168 -0
  122. package/dist/lib/query-builder.js.map +1 -0
  123. package/dist/lib/query-commands.d.ts +19 -0
  124. package/dist/lib/query-commands.js +262 -0
  125. package/dist/lib/query-commands.js.map +1 -0
  126. package/dist/lib/session.d.ts +205 -0
  127. package/dist/lib/session.js +1075 -0
  128. package/dist/lib/session.js.map +1 -0
  129. package/dist/lib/tokenize.d.ts +12 -0
  130. package/dist/lib/tokenize.js +48 -0
  131. package/dist/lib/tokenize.js.map +1 -0
  132. package/dist/lib/uiapi.d.ts +109 -0
  133. package/dist/lib/uiapi.js +159 -0
  134. package/dist/lib/uiapi.js.map +1 -0
  135. package/dist/lib/validator.d.ts +27 -0
  136. package/dist/lib/validator.js +100 -0
  137. package/dist/lib/validator.js.map +1 -0
  138. package/dist/lib/variable-promotion.d.ts +69 -0
  139. package/dist/lib/variable-promotion.js +95 -0
  140. package/dist/lib/variable-promotion.js.map +1 -0
  141. package/dist/lib/walker.d.ts +147 -0
  142. package/dist/lib/walker.js +723 -0
  143. package/dist/lib/walker.js.map +1 -0
  144. package/dist/mcp/server.d.ts +7 -0
  145. package/dist/mcp/server.js +34 -0
  146. package/dist/mcp/server.js.map +1 -0
  147. package/dist/mcp/stdio.d.ts +7 -0
  148. package/dist/mcp/stdio.js +25 -0
  149. package/dist/mcp/stdio.js.map +1 -0
  150. package/dist/mcp/tools/echo.d.ts +7 -0
  151. package/dist/mcp/tools/echo.js +17 -0
  152. package/dist/mcp/tools/echo.js.map +1 -0
  153. package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
  154. package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
  155. package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
  156. package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
  157. package/dist/mcp/tools/sf-gql-create.js +35 -0
  158. package/dist/mcp/tools/sf-gql-create.js.map +1 -0
  159. package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
  160. package/dist/mcp/tools/sf-gql-delete.js +31 -0
  161. package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
  162. package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
  163. package/dist/mcp/tools/sf-gql-detail.js +58 -0
  164. package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
  165. package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
  166. package/dist/mcp/tools/sf-gql-discover.js +51 -0
  167. package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
  168. package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
  169. package/dist/mcp/tools/sf-gql-list.js +53 -0
  170. package/dist/mcp/tools/sf-gql-list.js.map +1 -0
  171. package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
  172. package/dist/mcp/tools/sf-gql-raw.js +39 -0
  173. package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
  174. package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
  175. package/dist/mcp/tools/sf-gql-update.js +35 -0
  176. package/dist/mcp/tools/sf-gql-update.js.map +1 -0
  177. package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
  178. package/dist/mcp/tools/shared/zod-schemas.js +46 -0
  179. package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
  180. package/package.json +36 -0
  181. package/ralph-loop.sh +120 -0
  182. package/scripts/smoke-orderby.sh +190 -0
  183. package/src/__tests__/helpers/prime-deps.ts +46 -0
  184. package/src/__tests__/helpers/schema.ts +73 -0
  185. package/src/__tests__/helpers/stdout.ts +33 -0
  186. package/src/__tests__/setup.ts +19 -0
  187. package/src/cli.ts +764 -0
  188. package/src/commands/__tests__/query.spec.ts +137 -0
  189. package/src/commands/args.ts +306 -0
  190. package/src/commands/build.ts +288 -0
  191. package/src/commands/connect.ts +60 -0
  192. package/src/commands/describe.ts +246 -0
  193. package/src/commands/meta.ts +171 -0
  194. package/src/commands/navigate.ts +134 -0
  195. package/src/commands/query-helpers.ts +1202 -0
  196. package/src/commands/query.ts +1085 -0
  197. package/src/commands/review.ts +670 -0
  198. package/src/commands/session-mgmt.ts +256 -0
  199. package/src/commands/type.ts +437 -0
  200. package/src/commands/validate-input.ts +38 -0
  201. package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
  202. package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
  203. package/src/intent/__tests__/build-delete.spec.ts +121 -0
  204. package/src/intent/__tests__/build-detail.spec.ts +333 -0
  205. package/src/intent/__tests__/build-discover.spec.ts +432 -0
  206. package/src/intent/__tests__/build-list.spec.ts +284 -0
  207. package/src/intent/__tests__/build-mutation.spec.ts +108 -0
  208. package/src/intent/__tests__/build-output.spec.ts +55 -0
  209. package/src/intent/__tests__/build-raw.spec.ts +153 -0
  210. package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
  211. package/src/intent/build-aggregate.ts +182 -0
  212. package/src/intent/build-create.ts +19 -0
  213. package/src/intent/build-delete.ts +62 -0
  214. package/src/intent/build-detail.ts +95 -0
  215. package/src/intent/build-discover.ts +199 -0
  216. package/src/intent/build-list.ts +91 -0
  217. package/src/intent/build-mutation.ts +75 -0
  218. package/src/intent/build-output.ts +72 -0
  219. package/src/intent/build-raw.ts +61 -0
  220. package/src/intent/build-update.ts +19 -0
  221. package/src/intent/get-schema-with-priming.ts +43 -0
  222. package/src/intent/select-child-relationship.ts +48 -0
  223. package/src/intent/types.ts +181 -0
  224. package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
  225. package/src/lib/__tests__/apply-command.spec.ts +171 -0
  226. package/src/lib/__tests__/auth.spec.ts +292 -0
  227. package/src/lib/__tests__/formatter.spec.ts +86 -0
  228. package/src/lib/__tests__/graphql-name.spec.ts +64 -0
  229. package/src/lib/__tests__/introspect.spec.ts +399 -0
  230. package/src/lib/__tests__/object-info.spec.ts +124 -0
  231. package/src/lib/__tests__/path-selection.spec.ts +219 -0
  232. package/src/lib/__tests__/prime-schema.spec.ts +269 -0
  233. package/src/lib/__tests__/query-builder.spec.ts +95 -0
  234. package/src/lib/__tests__/query-commands.spec.ts +74 -0
  235. package/src/lib/__tests__/session.spec.ts +292 -0
  236. package/src/lib/__tests__/tokenize.spec.ts +33 -0
  237. package/src/lib/__tests__/uiapi.spec.ts +192 -0
  238. package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
  239. package/src/lib/__tests__/walker.spec.ts +250 -0
  240. package/src/lib/apply-command.ts +286 -0
  241. package/src/lib/auth.ts +157 -0
  242. package/src/lib/codegen.ts +899 -0
  243. package/src/lib/command-registry.ts +434 -0
  244. package/src/lib/formatter.ts +587 -0
  245. package/src/lib/fs-utils.ts +46 -0
  246. package/src/lib/graphql-name.ts +35 -0
  247. package/src/lib/interactive.ts +634 -0
  248. package/src/lib/introspect.ts +320 -0
  249. package/src/lib/object-info.ts +409 -0
  250. package/src/lib/path-selection.ts +162 -0
  251. package/src/lib/prime-schema.ts +195 -0
  252. package/src/lib/query-builder.ts +193 -0
  253. package/src/lib/query-commands.ts +290 -0
  254. package/src/lib/session.ts +1304 -0
  255. package/src/lib/tokenize.ts +43 -0
  256. package/src/lib/uiapi.ts +176 -0
  257. package/src/lib/validator.ts +143 -0
  258. package/src/lib/variable-promotion.ts +145 -0
  259. package/src/lib/walker.ts +975 -0
  260. package/src/mcp/__tests__/server.spec.ts +155 -0
  261. package/src/mcp/server.ts +38 -0
  262. package/src/mcp/stdio.ts +29 -0
  263. package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
  264. package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
  265. package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
  266. package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
  267. package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
  268. package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
  269. package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
  270. package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
  271. package/src/mcp/tools/echo.ts +24 -0
  272. package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
  273. package/src/mcp/tools/sf-gql-create.ts +55 -0
  274. package/src/mcp/tools/sf-gql-delete.ts +49 -0
  275. package/src/mcp/tools/sf-gql-detail.ts +85 -0
  276. package/src/mcp/tools/sf-gql-discover.ts +67 -0
  277. package/src/mcp/tools/sf-gql-list.ts +73 -0
  278. package/src/mcp/tools/sf-gql-raw.ts +56 -0
  279. package/src/mcp/tools/sf-gql-update.ts +55 -0
  280. package/src/mcp/tools/shared/zod-schemas.ts +55 -0
  281. package/tsconfig.json +18 -0
  282. package/vitest.config.ts +14 -0
@@ -0,0 +1,194 @@
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 path from "node:path";
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { buildSchema, introspectionFromSchema } from "graphql";
12
+ import { describe, expect, it } from "vitest";
13
+ import { atomicWriteJson } from "../../../lib/fs-utils.js";
14
+ import {
15
+ schemaCacheKeyForInstanceUrl,
16
+ schemaDir,
17
+ type SchemaMetadata,
18
+ } from "../../../lib/introspect.js";
19
+ import { type PrimeDeps } from "../../../lib/prime-schema.js";
20
+ import { primeSchemaCache } from "../../../lib/walker.js";
21
+ import { registerSfGqlDeleteTool } from "../sf-gql-delete.js";
22
+
23
+ const ORG = "test-tool-delete";
24
+ const ORG_URL = "https://test-tool-delete.my.salesforce.com";
25
+ const SCHEMA = buildSchema(`
26
+ type Query { _placeholder: Boolean }
27
+ type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! }
28
+ input UIAPIMutationsInput { allOrNone: Boolean }
29
+ type UIAPIMutations {
30
+ AccountDelete(input: RecordDeleteInput!): RecordDeletePayload
31
+ }
32
+ input RecordDeleteInput { Id: ID! }
33
+ type RecordDeletePayload { Id: ID }
34
+ type Account { Id: ID!, Name: StringValue }
35
+ type StringValue { value: String }
36
+ `);
37
+ primeSchemaCache(ORG, SCHEMA);
38
+ primeSchemaCache(ORG_URL, SCHEMA);
39
+
40
+ const primeDeps: PrimeDeps = {
41
+ getOrgAuth: async () => ({
42
+ alias: ORG,
43
+ username: "u",
44
+ instanceUrl: ORG_URL,
45
+ accessToken: "t",
46
+ orgId: "00D",
47
+ }),
48
+ downloadSchema: async (auth) => {
49
+ const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
50
+ const filePath = path.join(schemaDir(), `${cacheKey}.json`);
51
+ atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
52
+ const meta: SchemaMetadata = {
53
+ cacheKey,
54
+ instanceUrl: auth.instanceUrl,
55
+ typeCount: 0,
56
+ downloadedAt: new Date().toISOString(),
57
+ filePath,
58
+ };
59
+ return meta;
60
+ },
61
+ };
62
+
63
+ async function connect(): Promise<{ client: Client; server: McpServer }> {
64
+ const server = new McpServer({ name: "graphiti-mcp", version: "test" });
65
+ registerSfGqlDeleteTool(server, { primeDeps });
66
+ const [c, s] = InMemoryTransport.createLinkedPair();
67
+ const client = new Client({ name: "test", version: "0.0.0" });
68
+ await Promise.all([server.connect(s), client.connect(c)]);
69
+ return { client, server };
70
+ }
71
+
72
+ describe("mcp/tools/sf-gql-delete", () => {
73
+ it("tools/list advertises sf_gql_delete with org/object/inputVariable/operationName properties", async () => {
74
+ const { client, server } = await connect();
75
+ try {
76
+ const list = await client.listTools();
77
+ const tool = list.tools.find((t) => t.name === "sf_gql_delete");
78
+ expect(tool).toBeDefined();
79
+ const props =
80
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
81
+ expect(props.org).toBeDefined();
82
+ expect(props.object).toBeDefined();
83
+ expect(props.inputVariable).toBeDefined();
84
+ expect(props.operationName).toBeDefined();
85
+ // Delete has no returnFields — result is always Id only.
86
+ expect(props.returnFields).toBeUndefined();
87
+ } finally {
88
+ await client.close();
89
+ await server.close();
90
+ }
91
+ });
92
+
93
+ it("tools/call sf_gql_delete returns ToolOutput with RecordDeleteInput! and Id-only selection", async () => {
94
+ const { client, server } = await connect();
95
+ try {
96
+ const result = await client.callTool({
97
+ name: "sf_gql_delete",
98
+ arguments: { org: ORG, object: "Account" },
99
+ });
100
+ const content = result.content as { type: string; text?: string }[];
101
+ expect(content[0]?.type).toBe("text");
102
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
103
+ query: string;
104
+ variables: unknown[];
105
+ types: string;
106
+ };
107
+ expect(parsed.query).toMatch(/mutation DeleteAccount/);
108
+ expect(parsed.query).toMatch(/\$input:\s*RecordDeleteInput!/);
109
+ expect(parsed.query).toMatch(/AccountDelete\(input:\s*\$input\)/);
110
+ expect(parsed.query).toMatch(/\bId\b/);
111
+ // Id is a plain scalar on the payload — never a value wrapper, never a Record path.
112
+ expect(parsed.query).not.toMatch(/Id\s*\{\s*value/s);
113
+ expect(parsed.query).not.toMatch(/\bRecord\s*\{/);
114
+ expect(parsed.variables).toHaveLength(1);
115
+ expect(parsed.variables[0]).toEqual({
116
+ name: "input",
117
+ type: "RecordDeleteInput!",
118
+ required: true,
119
+ });
120
+ expect(typeof parsed.types).toBe("string");
121
+ } finally {
122
+ await client.close();
123
+ await server.close();
124
+ }
125
+ });
126
+
127
+ it("tools/call sf_gql_delete with custom inputVariable declares that variable", async () => {
128
+ const { client, server } = await connect();
129
+ try {
130
+ const result = await client.callTool({
131
+ name: "sf_gql_delete",
132
+ arguments: { org: ORG, object: "Account", inputVariable: "acctInput" },
133
+ });
134
+ const content = result.content as { type: string; text?: string }[];
135
+ const parsed = JSON.parse(content[0]?.text ?? "{}");
136
+ expect(parsed.query).toMatch(/\$acctInput:\s*RecordDeleteInput!/);
137
+ expect(parsed.query).toMatch(/input:\s*\$acctInput/);
138
+ } finally {
139
+ await client.close();
140
+ await server.close();
141
+ }
142
+ });
143
+
144
+ it("tools/call sf_gql_delete with custom operationName uses it", async () => {
145
+ const { client, server } = await connect();
146
+ try {
147
+ const result = await client.callTool({
148
+ name: "sf_gql_delete",
149
+ arguments: { org: ORG, object: "Account", operationName: "RemoveAccount" },
150
+ });
151
+ const content = result.content as { type: string; text?: string }[];
152
+ const parsed = JSON.parse(content[0]?.text ?? "{}");
153
+ expect(parsed.query).toMatch(/mutation RemoveAccount/);
154
+ } finally {
155
+ await client.close();
156
+ await server.close();
157
+ }
158
+ });
159
+
160
+ it("tools/call sf_gql_delete strips leading $ from inputVariable", async () => {
161
+ const { client, server } = await connect();
162
+ try {
163
+ const result = await client.callTool({
164
+ name: "sf_gql_delete",
165
+ arguments: { org: ORG, object: "Account", inputVariable: "$myInput" },
166
+ });
167
+ expect(result.isError).toBeFalsy();
168
+ const content = result.content as { type: string; text?: string }[];
169
+ const parsed = JSON.parse(content[0]?.text ?? "{}");
170
+ expect(parsed.query).toMatch(/\$myInput:\s*RecordDeleteInput!/);
171
+ expect(parsed.query).toMatch(/input:\s*\$myInput/);
172
+ expect(parsed.query).not.toMatch(/\$\$myInput/);
173
+ } finally {
174
+ await client.close();
175
+ await server.close();
176
+ }
177
+ });
178
+
179
+ it("tools/call sf_gql_delete with an invalid object name returns error", async () => {
180
+ const { client, server } = await connect();
181
+ try {
182
+ const result = await client.callTool({
183
+ name: "sf_gql_delete",
184
+ arguments: { org: ORG, object: "my-object" },
185
+ });
186
+ expect(result.isError).toBe(true);
187
+ const content = result.content as { type: string; text?: string }[];
188
+ expect(content[0]?.text).toMatch(/is not a valid GraphQL Name/);
189
+ } finally {
190
+ await client.close();
191
+ await server.close();
192
+ }
193
+ });
194
+ });
@@ -0,0 +1,246 @@
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 path from "node:path";
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { buildSchema, introspectionFromSchema } from "graphql";
12
+ import { describe, expect, it } from "vitest";
13
+ import { atomicWriteJson } from "../../../lib/fs-utils.js";
14
+ import {
15
+ schemaCacheKeyForInstanceUrl,
16
+ schemaDir,
17
+ type SchemaMetadata,
18
+ } from "../../../lib/introspect.js";
19
+ import { type PrimeDeps } from "../../../lib/prime-schema.js";
20
+ import { primeSchemaCache } from "../../../lib/walker.js";
21
+ import { registerSfGqlDetailTool } from "../sf-gql-detail.js";
22
+
23
+ const ORG = "test-tool-detail";
24
+ const ORG_URL = "https://test-tool-detail.my.salesforce.com";
25
+ const SCHEMA = buildSchema(`
26
+ type Query { uiapi: UIAPI! }
27
+ type UIAPI { query: RecordQuery! }
28
+ type RecordQuery { Account(first: Int, where: Account_Filter): AccountConnection! }
29
+ input Account_Filter { Id: IDOperators }
30
+ input IDOperators { eq: ID, ne: ID }
31
+ enum Order { ASC DESC }
32
+ input OrderByClause { order: Order!, nulls: NullsOrder }
33
+ enum NullsOrder { FIRST LAST }
34
+ input Contact_OrderBy { LastName: OrderByClause, Title: OrderByClause }
35
+ type AccountConnection { edges: [AccountEdge!]! }
36
+ type AccountEdge { node: Account! }
37
+ type Account {
38
+ Id: ID!
39
+ Name: StringValue
40
+ Contacts(first: Int, orderBy: Contact_OrderBy): ContactConnection
41
+ }
42
+ type ContactConnection { edges: [ContactEdge!]! }
43
+ type ContactEdge { node: Contact! }
44
+ type Contact { Id: ID!, LastName: StringValue }
45
+ type StringValue { value: String }
46
+ `);
47
+ primeSchemaCache(ORG, SCHEMA);
48
+ primeSchemaCache(ORG_URL, SCHEMA);
49
+
50
+ const primeDeps: PrimeDeps = {
51
+ getOrgAuth: async () => ({
52
+ alias: ORG,
53
+ username: "u",
54
+ instanceUrl: ORG_URL,
55
+ accessToken: "t",
56
+ orgId: "00D",
57
+ }),
58
+ downloadSchema: async (auth) => {
59
+ const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
60
+ const filePath = path.join(schemaDir(), `${cacheKey}.json`);
61
+ atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
62
+ const meta: SchemaMetadata = {
63
+ cacheKey,
64
+ instanceUrl: auth.instanceUrl,
65
+ typeCount: 0,
66
+ downloadedAt: new Date().toISOString(),
67
+ filePath,
68
+ };
69
+ return meta;
70
+ },
71
+ };
72
+
73
+ async function connect(): Promise<{ client: Client; server: McpServer }> {
74
+ const server = new McpServer({ name: "graphiti-mcp", version: "test" });
75
+ registerSfGqlDetailTool(server, { primeDeps });
76
+ const [c, s] = InMemoryTransport.createLinkedPair();
77
+ const client = new Client({ name: "test", version: "0.0.0" });
78
+ await Promise.all([server.connect(s), client.connect(c)]);
79
+ return { client, server };
80
+ }
81
+
82
+ describe("mcp/tools/sf-gql-detail", () => {
83
+ it("tools/list advertises sf_gql_detail with detail-shaped properties", async () => {
84
+ const { client, server } = await connect();
85
+ try {
86
+ const list = await client.listTools();
87
+ const tool = list.tools.find((t) => t.name === "sf_gql_detail");
88
+ expect(tool).toBeDefined();
89
+ const props =
90
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
91
+ expect(props.org).toBeDefined();
92
+ expect(props.object).toBeDefined();
93
+ expect(props.fields).toBeDefined();
94
+ expect(props.idVariable).toBeDefined();
95
+ // Negative: detail does not advertise list/aggregate-shaped knobs.
96
+ expect(props.filter).toBeUndefined();
97
+ expect(props.orderBy).toBeUndefined();
98
+ expect(props.scope).toBeUndefined();
99
+ expect(props.first).toBeUndefined();
100
+ } finally {
101
+ await client.close();
102
+ await server.close();
103
+ }
104
+ });
105
+
106
+ it("tools/call sf_gql_detail returns ToolOutput envelope with $id: ID! required", async () => {
107
+ const { client, server } = await connect();
108
+ try {
109
+ const result = await client.callTool({
110
+ name: "sf_gql_detail",
111
+ arguments: { org: ORG, object: "Account", fields: ["Id", "Name"] },
112
+ });
113
+ const content = result.content as { type: string; text?: string }[];
114
+ expect(content[0]?.type).toBe("text");
115
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
116
+ query: string;
117
+ variables: { name: string; type: string; required: boolean }[];
118
+ types: string;
119
+ warnings: string[];
120
+ };
121
+ expect(parsed.query).toMatch(/\bAccountDetail\b/);
122
+ expect(parsed.query).toMatch(/Name\s*\{\s*value\s*\}/);
123
+ expect(parsed.query).toMatch(/first\s*:\s*1\b/);
124
+ const id = parsed.variables.find((v) => v.name === "id");
125
+ expect(id).toBeDefined();
126
+ expect(id!.type).toBe("ID!");
127
+ expect(id!.required).toBe(true);
128
+ } finally {
129
+ await client.close();
130
+ await server.close();
131
+ }
132
+ });
133
+
134
+ it("tools/call sf_gql_detail with custom idVariable round-trips", async () => {
135
+ const { client, server } = await connect();
136
+ try {
137
+ const result = await client.callTool({
138
+ name: "sf_gql_detail",
139
+ arguments: {
140
+ org: ORG,
141
+ object: "Account",
142
+ fields: ["Id"],
143
+ idVariable: "accountId",
144
+ },
145
+ });
146
+ const content = result.content as { type: string; text?: string }[];
147
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
148
+ query: string;
149
+ variables: { name: string; type: string }[];
150
+ };
151
+ expect(parsed.query).toMatch(/\$accountId\s*:\s*ID!/);
152
+ expect(parsed.query).toMatch(/eq\s*:\s*\$accountId/);
153
+ expect(parsed.variables.find((v) => v.name === "accountId")).toBeDefined();
154
+ expect(parsed.variables.find((v) => v.name === "id")).toBeUndefined();
155
+ } finally {
156
+ await client.close();
157
+ await server.close();
158
+ }
159
+ });
160
+
161
+ it("tools/call sf_gql_detail with missing fields returns validation error", async () => {
162
+ const { client, server } = await connect();
163
+ try {
164
+ const result = await client.callTool({
165
+ name: "sf_gql_detail",
166
+ arguments: { org: ORG, object: "Account" },
167
+ });
168
+ expect(result.isError).toBe(true);
169
+ const content = result.content as { type: string; text?: string }[];
170
+ expect(content[0]?.text ?? "").toMatch(/fields/);
171
+ } finally {
172
+ await client.close();
173
+ await server.close();
174
+ }
175
+ });
176
+
177
+ it("tools/call rejects invalid GraphQL Names in object/idVariable/operationName", async () => {
178
+ const { client, server } = await connect();
179
+ try {
180
+ for (const args of [
181
+ { org: ORG, object: "Order Item", fields: ["Id"] },
182
+ { org: ORG, object: "Account", fields: ["Id"], idVariable: "my-id" },
183
+ { org: ORG, object: "Account", fields: ["Id"], operationName: "1Bad" },
184
+ ]) {
185
+ const result = await client.callTool({ name: "sf_gql_detail", arguments: args });
186
+ expect(result.isError).toBe(true);
187
+ const content = result.content as { type: string; text?: string }[];
188
+ expect(content[0]?.text ?? "").toMatch(/GraphQL Name/);
189
+ }
190
+ } finally {
191
+ await client.close();
192
+ await server.close();
193
+ }
194
+ });
195
+
196
+ it("tools/list advertises childRelationships.orderBy as a singleton object (FR-6.3)", async () => {
197
+ const { client, server } = await connect();
198
+ try {
199
+ const list = await client.listTools();
200
+ const tool = list.tools.find((t) => t.name === "sf_gql_detail");
201
+ const props =
202
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
203
+ const child = props.childRelationships as {
204
+ items?: { properties?: { orderBy?: { type?: string; anyOf?: unknown[] } } };
205
+ };
206
+ const orderBy = child?.items?.properties?.orderBy;
207
+ expect(orderBy).toBeDefined();
208
+ // Singleton form: the advertised schema has a single object type, not a union with array.
209
+ expect(orderBy?.anyOf).toBeUndefined();
210
+ expect(orderBy?.type).toBe("object");
211
+ } finally {
212
+ await client.close();
213
+ await server.close();
214
+ }
215
+ });
216
+
217
+ it("tools/call still accepts childRelationships.orderBy as an array at runtime (FR-6.2 shim)", async () => {
218
+ const { client, server } = await connect();
219
+ try {
220
+ const result = await client.callTool({
221
+ name: "sf_gql_detail",
222
+ arguments: {
223
+ org: ORG,
224
+ object: "Account",
225
+ fields: ["Id"],
226
+ childRelationships: [
227
+ {
228
+ relationshipName: "Contacts",
229
+ fields: ["Id"],
230
+ orderBy: [{ LastName: { order: "ASC" } }, { Title: { order: "DESC" } }],
231
+ },
232
+ ],
233
+ },
234
+ });
235
+ expect(result.isError).toBeFalsy();
236
+ // Array was collapsed before reaching the renderer; only the first key shows up.
237
+ const content = result.content as { type: string; text?: string }[];
238
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string };
239
+ expect(parsed.query).toMatch(/orderBy\s*:\s*\{\s*LastName/);
240
+ expect(parsed.query).not.toMatch(/Title/);
241
+ } finally {
242
+ await client.close();
243
+ await server.close();
244
+ }
245
+ });
246
+ });