@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,320 @@
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 ObjectInfoResult } from "../../../lib/object-info.js";
20
+ import { type PrimeDeps } from "../../../lib/prime-schema.js";
21
+ import { primeSchemaCache } from "../../../lib/walker.js";
22
+ import { registerSfGqlDiscoverTool } from "../sf-gql-discover.js";
23
+
24
+ const ORG = "test-tool-discover";
25
+ const ORG_URL = "https://test-tool-discover.my.salesforce.com";
26
+ const SCHEMA = buildSchema(`
27
+ type Query { uiapi: UIAPI! }
28
+ type UIAPI { query: RecordQuery! }
29
+ type RecordQuery {
30
+ Account(first: Int, after: String): AccountConnection!
31
+ }
32
+ type AccountConnection { edges: [AccountEdge!]! }
33
+ type AccountEdge { node: Account! }
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
+ const ACCOUNT_INFO: ObjectInfoResult = {
64
+ apiName: "Account",
65
+ label: "Account",
66
+ labelPlural: "Accounts",
67
+ createable: true,
68
+ deletable: true,
69
+ updateable: true,
70
+ queryable: true,
71
+ searchable: true,
72
+ custom: false,
73
+ keyPrefix: "001",
74
+ nameFields: ["Name"],
75
+ defaultRecordTypeId: null,
76
+ fields: [
77
+ {
78
+ apiName: "Id",
79
+ label: "Account ID",
80
+ dataType: "ID",
81
+ required: false,
82
+ createable: false,
83
+ updateable: false,
84
+ calculated: false,
85
+ custom: false,
86
+ filterable: true,
87
+ sortable: true,
88
+ nameField: false,
89
+ reference: false,
90
+ relationshipName: null,
91
+ compound: false,
92
+ compoundFieldName: null,
93
+ defaultedOnCreate: false,
94
+ extraTypeInfo: null,
95
+ inlineHelpText: null,
96
+ precision: 0,
97
+ scale: 0,
98
+ referenceToInfos: [],
99
+ controllerName: null,
100
+ controllingFields: [],
101
+ },
102
+ ],
103
+ childRelationships: [],
104
+ recordTypeInfos: [],
105
+ picklists: [],
106
+ fetchedAt: new Date().toISOString(),
107
+ };
108
+
109
+ async function connect(): Promise<{ client: Client; server: McpServer }> {
110
+ const server = new McpServer({ name: "graphiti-mcp", version: "test" });
111
+ registerSfGqlDiscoverTool(server, {
112
+ primeDeps,
113
+ getOrgAuth: async () => ({
114
+ alias: ORG,
115
+ username: "u",
116
+ instanceUrl: ORG_URL,
117
+ accessToken: "t",
118
+ orgId: "00D",
119
+ }),
120
+ getObjectInfo: async (_auth, _alias, name) => {
121
+ if (name === "Account") return ACCOUNT_INFO;
122
+ throw new Error(`No fixture for ${name}`);
123
+ },
124
+ });
125
+ const [c, s] = InMemoryTransport.createLinkedPair();
126
+ const client = new Client({ name: "test", version: "0.0.0" });
127
+ await Promise.all([server.connect(s), client.connect(c)]);
128
+ return { client, server };
129
+ }
130
+
131
+ describe("mcp/tools/sf-gql-discover", () => {
132
+ it("tools/list advertises sf_gql_discover with org/mode properties", async () => {
133
+ const { client, server } = await connect();
134
+ try {
135
+ const list = await client.listTools();
136
+ const tool = list.tools.find((t) => t.name === "sf_gql_discover");
137
+ expect(tool).toBeDefined();
138
+ const props =
139
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
140
+ expect(props.org).toBeDefined();
141
+ expect(props.mode).toBeDefined();
142
+ } finally {
143
+ await client.close();
144
+ await server.close();
145
+ }
146
+ });
147
+
148
+ it("tools/call list_objects returns the schema's queryable SObjects", async () => {
149
+ const { client, server } = await connect();
150
+ try {
151
+ const result = await client.callTool({
152
+ name: "sf_gql_discover",
153
+ arguments: { org: ORG, mode: "list_objects" },
154
+ });
155
+ const content = result.content as { type: string; text?: string }[];
156
+ expect(content[0]?.type).toBe("text");
157
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
158
+ mode: string;
159
+ objects: { name: string }[];
160
+ };
161
+ expect(parsed.mode).toBe("list_objects");
162
+ expect(parsed.objects.map((o) => o.name)).toContain("Account");
163
+ } finally {
164
+ await client.close();
165
+ await server.close();
166
+ }
167
+ });
168
+
169
+ it("tools/call describe_object returns ObjectDescription", async () => {
170
+ const { client, server } = await connect();
171
+ try {
172
+ const result = await client.callTool({
173
+ name: "sf_gql_discover",
174
+ arguments: { org: ORG, mode: "describe_object", object: "Account" },
175
+ });
176
+ const content = result.content as { type: string; text?: string }[];
177
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
178
+ mode: string;
179
+ object: { name: string; fields: { name: string }[] };
180
+ };
181
+ expect(parsed.mode).toBe("describe_object");
182
+ expect(parsed.object.name).toBe("Account");
183
+ expect(parsed.object.fields.map((f) => f.name)).toContain("Id");
184
+ } finally {
185
+ await client.close();
186
+ await server.close();
187
+ }
188
+ });
189
+
190
+ it("tools/call with missing required arg returns validation error", async () => {
191
+ const { client, server } = await connect();
192
+ try {
193
+ const result = await client.callTool({
194
+ name: "sf_gql_discover",
195
+ arguments: { org: ORG },
196
+ });
197
+ expect(result.isError).toBe(true);
198
+ const content = result.content as { type: string; text?: string }[];
199
+ expect(content[0]?.text ?? "").toMatch(/mode/);
200
+ } finally {
201
+ await client.close();
202
+ await server.close();
203
+ }
204
+ });
205
+
206
+ it("tools/call describe_object without object returns error", async () => {
207
+ const { client, server } = await connect();
208
+ try {
209
+ const result = await client.callTool({
210
+ name: "sf_gql_discover",
211
+ arguments: { org: ORG, mode: "describe_object" },
212
+ });
213
+ expect(result.isError).toBe(true);
214
+ const content = result.content as { type: string; text?: string }[];
215
+ expect(content[0]?.text ?? "").toMatch(/object/);
216
+ } finally {
217
+ await client.close();
218
+ await server.close();
219
+ }
220
+ });
221
+
222
+ it("rejects org alias containing shell metacharacters", async () => {
223
+ const { client, server } = await connect();
224
+ try {
225
+ const result = await client.callTool({
226
+ name: "sf_gql_discover",
227
+ arguments: { org: "evil; rm -rf /", mode: "list_objects" },
228
+ });
229
+ expect(result.isError).toBe(true);
230
+ const content = result.content as { type: string; text?: string }[];
231
+ expect(content[0]?.text ?? "").toMatch(/org/);
232
+ } finally {
233
+ await client.close();
234
+ await server.close();
235
+ }
236
+ });
237
+
238
+ it("rejects object name containing path traversal", async () => {
239
+ const { client, server } = await connect();
240
+ try {
241
+ const result = await client.callTool({
242
+ name: "sf_gql_discover",
243
+ arguments: { org: ORG, mode: "describe_object", object: "../etc/passwd" },
244
+ });
245
+ expect(result.isError).toBe(true);
246
+ const content = result.content as { type: string; text?: string }[];
247
+ expect(content[0]?.text ?? "").toMatch(/object/);
248
+ } finally {
249
+ await client.close();
250
+ await server.close();
251
+ }
252
+ });
253
+
254
+ it("accepts org alias at the 80-char limit", async () => {
255
+ const { client, server } = await connect();
256
+ try {
257
+ const longOrg = "a".repeat(80);
258
+ const result = await client.callTool({
259
+ name: "sf_gql_discover",
260
+ arguments: { org: longOrg, mode: "list_objects" },
261
+ });
262
+ // Org isn't primed so the call fails downstream, but it must
263
+ // pass zod validation — surface as a non-validation error.
264
+ const content = result.content as { type: string; text?: string }[];
265
+ expect(content[0]?.text ?? "").not.toMatch(/org must match/);
266
+ } finally {
267
+ await client.close();
268
+ await server.close();
269
+ }
270
+ });
271
+
272
+ it("rejects org alias one over the 80-char limit", async () => {
273
+ const { client, server } = await connect();
274
+ try {
275
+ const tooLongOrg = "a".repeat(81);
276
+ const result = await client.callTool({
277
+ name: "sf_gql_discover",
278
+ arguments: { org: tooLongOrg, mode: "list_objects" },
279
+ });
280
+ expect(result.isError).toBe(true);
281
+ const content = result.content as { type: string; text?: string }[];
282
+ expect(content[0]?.text ?? "").toMatch(/org/);
283
+ } finally {
284
+ await client.close();
285
+ await server.close();
286
+ }
287
+ });
288
+
289
+ it("rejects search containing control characters", async () => {
290
+ const { client, server } = await connect();
291
+ try {
292
+ const result = await client.callTool({
293
+ name: "sf_gql_discover",
294
+ arguments: { org: ORG, mode: "list_objects", search: "foo\x00bar" },
295
+ });
296
+ expect(result.isError).toBe(true);
297
+ const content = result.content as { type: string; text?: string }[];
298
+ expect(content[0]?.text ?? "").toMatch(/search/);
299
+ } finally {
300
+ await client.close();
301
+ await server.close();
302
+ }
303
+ });
304
+
305
+ it("rejects search exceeding 100 characters", async () => {
306
+ const { client, server } = await connect();
307
+ try {
308
+ const result = await client.callTool({
309
+ name: "sf_gql_discover",
310
+ arguments: { org: ORG, mode: "list_objects", search: "a".repeat(101) },
311
+ });
312
+ expect(result.isError).toBe(true);
313
+ const content = result.content as { type: string; text?: string }[];
314
+ expect(content[0]?.text ?? "").toMatch(/search/);
315
+ } finally {
316
+ await client.close();
317
+ await server.close();
318
+ }
319
+ });
320
+ });
@@ -0,0 +1,128 @@
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 { registerSfGqlListTool } from "../sf-gql-list.js";
22
+
23
+ const ORG = "test-tool-list";
24
+ const ORG_URL = "https://test-tool-list.my.salesforce.com";
25
+ const SCHEMA = buildSchema(`
26
+ type Query { uiapi: UIAPI! }
27
+ type UIAPI { query: RecordQuery! }
28
+ type RecordQuery { Account(first: Int, after: String): AccountConnection! }
29
+ type AccountConnection { edges: [AccountEdge!]!, pageInfo: PageInfo! }
30
+ type AccountEdge { node: Account! }
31
+ type PageInfo { hasNextPage: Boolean!, endCursor: String }
32
+ type Account { Id: ID!, Name: StringValue }
33
+ type StringValue { value: String }
34
+ `);
35
+ primeSchemaCache(ORG, SCHEMA);
36
+ primeSchemaCache(ORG_URL, SCHEMA);
37
+
38
+ const primeDeps: PrimeDeps = {
39
+ getOrgAuth: async () => ({
40
+ alias: ORG,
41
+ username: "u",
42
+ instanceUrl: ORG_URL,
43
+ accessToken: "t",
44
+ orgId: "00D",
45
+ }),
46
+ downloadSchema: async (auth) => {
47
+ const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
48
+ const filePath = path.join(schemaDir(), `${cacheKey}.json`);
49
+ atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
50
+ const meta: SchemaMetadata = {
51
+ cacheKey,
52
+ instanceUrl: auth.instanceUrl,
53
+ typeCount: 0,
54
+ downloadedAt: new Date().toISOString(),
55
+ filePath,
56
+ };
57
+ return meta;
58
+ },
59
+ };
60
+
61
+ async function connect(): Promise<{ client: Client; server: McpServer }> {
62
+ const server = new McpServer({ name: "graphiti-mcp", version: "test" });
63
+ registerSfGqlListTool(server, { primeDeps });
64
+ const [c, s] = InMemoryTransport.createLinkedPair();
65
+ const client = new Client({ name: "test", version: "0.0.0" });
66
+ await Promise.all([server.connect(s), client.connect(c)]);
67
+ return { client, server };
68
+ }
69
+
70
+ describe("mcp/tools/sf-gql-list", () => {
71
+ it("tools/list advertises sf_gql_list with org/object/fields properties", async () => {
72
+ const { client, server } = await connect();
73
+ try {
74
+ const list = await client.listTools();
75
+ const tool = list.tools.find((t) => t.name === "sf_gql_list");
76
+ expect(tool).toBeDefined();
77
+ const props =
78
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
79
+ expect(props.org).toBeDefined();
80
+ expect(props.object).toBeDefined();
81
+ expect(props.fields).toBeDefined();
82
+ } finally {
83
+ await client.close();
84
+ await server.close();
85
+ }
86
+ });
87
+
88
+ it("tools/call sf_gql_list returns ToolOutput JSON envelope", async () => {
89
+ const { client, server } = await connect();
90
+ try {
91
+ const result = await client.callTool({
92
+ name: "sf_gql_list",
93
+ arguments: { org: ORG, object: "Account", fields: ["Id", "Name"] },
94
+ });
95
+ const content = result.content as { type: string; text?: string }[];
96
+ expect(content[0]?.type).toBe("text");
97
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as {
98
+ query: string;
99
+ variables: unknown[];
100
+ types: string;
101
+ warnings: string[];
102
+ };
103
+ expect(parsed.query).toMatch(/\bAccountList\b/);
104
+ expect(parsed.query).toMatch(/Name\s*\{\s*value\s*\}/);
105
+ expect(Array.isArray(parsed.variables)).toBe(true);
106
+ expect(typeof parsed.types).toBe("string");
107
+ } finally {
108
+ await client.close();
109
+ await server.close();
110
+ }
111
+ });
112
+
113
+ it("tools/call sf_gql_list with missing required arg returns validation error", async () => {
114
+ const { client, server } = await connect();
115
+ try {
116
+ const result = await client.callTool({
117
+ name: "sf_gql_list",
118
+ arguments: { org: ORG, object: "Account" },
119
+ });
120
+ expect(result.isError).toBe(true);
121
+ const content = result.content as { type: string; text?: string }[];
122
+ expect(content[0]?.text ?? "").toMatch(/fields/);
123
+ } finally {
124
+ await client.close();
125
+ await server.close();
126
+ }
127
+ });
128
+ });
@@ -0,0 +1,141 @@
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 { createServer } from "../../server.js";
22
+ import { registerSfGqlRawTool } from "../sf-gql-raw.js";
23
+
24
+ const ORG = "test-tool-raw";
25
+ const ORG_URL = "https://test-tool-raw.my.salesforce.com";
26
+ const SCHEMA = buildSchema(`
27
+ type Query { uiapi: UIAPI! }
28
+ type UIAPI { query: RecordQuery! }
29
+ type RecordQuery { Case(first: Int, after: String, where: Case_Filter): CaseConnection! }
30
+ input Case_Filter { Status: PicklistOperators }
31
+ input PicklistOperators { eq: String }
32
+ type CaseConnection { edges: [CaseEdge!]!, pageInfo: PageInfo! }
33
+ type CaseEdge { node: Case! }
34
+ type PageInfo { hasNextPage: Boolean!, endCursor: String }
35
+ type Case { Id: ID!, Subject: StringValue, Status: StringValue }
36
+ type StringValue { value: String }
37
+ `);
38
+ primeSchemaCache(ORG, SCHEMA);
39
+ primeSchemaCache(ORG_URL, SCHEMA);
40
+
41
+ const primeDeps: PrimeDeps = {
42
+ getOrgAuth: async () => ({
43
+ alias: ORG,
44
+ username: "u",
45
+ instanceUrl: ORG_URL,
46
+ accessToken: "t",
47
+ orgId: "00D",
48
+ }),
49
+ downloadSchema: async (auth) => {
50
+ const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
51
+ const filePath = path.join(schemaDir(), `${cacheKey}.json`);
52
+ atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
53
+ const meta: SchemaMetadata = {
54
+ cacheKey,
55
+ instanceUrl: auth.instanceUrl,
56
+ typeCount: 0,
57
+ downloadedAt: new Date().toISOString(),
58
+ filePath,
59
+ };
60
+ return meta;
61
+ },
62
+ };
63
+
64
+ async function connect(): Promise<{ client: Client; server: McpServer }> {
65
+ const server = new McpServer({ name: "graphiti-mcp", version: "test" });
66
+ registerSfGqlRawTool(server, { primeDeps });
67
+ const [c, s] = InMemoryTransport.createLinkedPair();
68
+ const client = new Client({ name: "test", version: "0.0.0" });
69
+ await Promise.all([server.connect(s), client.connect(c)]);
70
+ return { client, server };
71
+ }
72
+
73
+ describe("mcp/tools/sf-gql-raw", () => {
74
+ it("tools/list advertises sf_gql_raw with org/commands properties", async () => {
75
+ const { client, server } = await connect();
76
+ try {
77
+ const list = await client.listTools();
78
+ const tool = list.tools.find((t) => t.name === "sf_gql_raw");
79
+ expect(tool).toBeDefined();
80
+ const props =
81
+ (tool!.inputSchema as { properties?: Record<string, unknown> }).properties ?? {};
82
+ expect(props.org).toBeDefined();
83
+ expect(props.commands).toBeDefined();
84
+ } finally {
85
+ await client.close();
86
+ await server.close();
87
+ }
88
+ });
89
+
90
+ it("tools/call sf_gql_raw renders a query from commands", async () => {
91
+ const { client, server } = await connect();
92
+ try {
93
+ const result = await client.callTool({
94
+ name: "sf_gql_raw",
95
+ arguments: {
96
+ org: ORG,
97
+ commands: ["select uiapi/query/Case/edges/node/Subject/value"],
98
+ },
99
+ });
100
+ expect(result.isError).toBeFalsy();
101
+ const content = result.content as { type: string; text?: string }[];
102
+ const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string };
103
+ expect(parsed.query).toMatch(/Subject\s*\{\s*value\s*\}/);
104
+ } finally {
105
+ await client.close();
106
+ await server.close();
107
+ }
108
+ });
109
+
110
+ it("tools/call sf_gql_raw fails fast on a bad command", async () => {
111
+ const { client, server } = await connect();
112
+ try {
113
+ const result = await client.callTool({
114
+ name: "sf_gql_raw",
115
+ arguments: { org: ORG, commands: ["bogus verb here"] },
116
+ });
117
+ expect(result.isError).toBe(true);
118
+ const content = result.content as { type: string; text?: string }[];
119
+ expect(content[0]?.text).toMatch(/command 0 \(bogus verb here\)/);
120
+ } finally {
121
+ await client.close();
122
+ await server.close();
123
+ }
124
+ });
125
+ });
126
+
127
+ describe("mcp/server registers sf_gql_raw", () => {
128
+ it("createServer advertises sf_gql_raw in tools/list", async () => {
129
+ const server = createServer();
130
+ const [c, s] = InMemoryTransport.createLinkedPair();
131
+ const client = new Client({ name: "test", version: "0.0.0" });
132
+ await Promise.all([server.connect(s), client.connect(c)]);
133
+ try {
134
+ const list = await client.listTools();
135
+ expect(list.tools.find((t) => t.name === "sf_gql_raw")).toBeDefined();
136
+ } finally {
137
+ await client.close();
138
+ await server.close();
139
+ }
140
+ });
141
+ });