/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { buildSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { makeNoopPrimeDeps } from "../../../__tests__/helpers/prime-deps.js"; import { primeSchemaCache } from "../../../lib/walker.js"; import { registerSfGqlUpdateTool } from "../sf-gql-update.js"; const ORG = "test-tool-update"; const ORG_URL = "https://test-tool-update.my.salesforce.com"; const SCHEMA = buildSchema(` type Query { _placeholder: Boolean } type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! } input UIAPIMutationsInput { allOrNone: Boolean } type UIAPIMutations { AccountUpdate(input: AccountUpdateInput!): AccountUpdatePayload } input AccountUpdateInput { Account: AccountUpdateRepresentation! } input AccountUpdateRepresentation { Name: String, Industry: String } type AccountUpdatePayload { Record: Account } type Account { Id: ID!, Name: StringValue, Industry: StringValue, Owner: User } type StringValue { value: String } type User { Id: ID!, Name: StringValue } `); primeSchemaCache(ORG, SCHEMA); primeSchemaCache(ORG_URL, SCHEMA); const primeDeps = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA); async function connect(): Promise<{ client: Client; server: McpServer }> { const server = new McpServer({ name: "graphiti-mcp", version: "test" }); registerSfGqlUpdateTool(server, { primeDeps }); const [c, s] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test", version: "0.0.0" }); await Promise.all([server.connect(s), client.connect(c)]); return { client, server }; } describe("mcp/tools/sf-gql-update", () => { it("tools/list advertises sf_gql_update with org/object/returnFields properties", async () => { const { client, server } = await connect(); try { const list = await client.listTools(); const tool = list.tools.find((t) => t.name === "sf_gql_update"); expect(tool).toBeDefined(); const props = (tool!.inputSchema as { properties?: Record }).properties ?? {}; expect(props.org).toBeDefined(); expect(props.object).toBeDefined(); expect(props.returnFields).toBeDefined(); // W-22899736: object + operationName advertise the GraphQL Name pattern in tools/list. expect((props.object as { pattern?: string }).pattern).toBe("^[A-Za-z_][A-Za-z0-9_]*$"); expect((props.operationName as { pattern?: string }).pattern).toBe( "^[A-Za-z_][A-Za-z0-9_]*$", ); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update returns ToolOutput with mutation keyword and value wrappers", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", returnFields: ["Id", "Name", "Industry"], }, }); const content = result.content as { type: string; text?: string }[]; expect(content[0]?.type).toBe("text"); const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string; variables: unknown[]; types: string; }; expect(parsed.query).toMatch(/mutation UpdateAccount/); expect(parsed.query).toMatch(/\$input:\s*AccountUpdateInput!/); expect(parsed.query).toMatch(/AccountUpdate\(input:\s*\$input\)/); expect(parsed.query).toMatch(/Name\s+@optional\s*\{\s*value/s); // Id is FLS-exempt: selected bare, never @optional (W-22818723). expect(parsed.query).not.toMatch(/\bId\s+@optional\b/); expect(parsed.variables).toHaveLength(1); expect(parsed.variables[0]).toEqual({ name: "input", type: "AccountUpdateInput!", required: true, }); expect(typeof parsed.types).toBe("string"); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update with no returnFields defaults to Id only", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account" }, }); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}"); // Id is FLS-exempt: selected bare, never @optional (W-22818723). expect(parsed.query).toMatch(/\bId\b/); expect(parsed.query).not.toMatch(/\bId\s+@optional\b/); expect(parsed.query).not.toMatch(/Name\s*\{/); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update with custom inputVariable declares that variable", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", inputVariable: "accountInput" }, }); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}"); expect(parsed.query).toMatch(/\$accountInput:\s*AccountUpdateInput!/); expect(parsed.query).toMatch(/input:\s*\$accountInput/); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update with custom operationName uses it", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", operationName: "MutateAccount" }, }); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}"); expect(parsed.query).toMatch(/mutation MutateAccount/); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update strips leading $ from inputVariable", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", inputVariable: "$myInput" }, }); expect(result.isError).toBeFalsy(); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}"); expect(parsed.query).toMatch(/\$myInput:\s*AccountUpdateInput!/); expect(parsed.query).toMatch(/input:\s*\$myInput/); expect(parsed.query).not.toMatch(/\$\$myInput/); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update with dotted returnField surfaces walker error as warning", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", returnFields: ["Id", "Owner.Name"] }, }); expect(result.isError).toBeFalsy(); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string; warnings: string[]; }; expect(parsed.query).toMatch(/\bId\b/); expect(parsed.warnings.some((w) => w.includes("not available in mutation results"))).toBe( true, ); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_update with empty returnFields returns error", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_update", arguments: { org: ORG, object: "Account", returnFields: [] }, }); expect(result.isError).toBe(true); const content = result.content as { type: string; text?: string }[]; expect(content[0]?.text).toMatch(/returnFields must contain at least one field/); } finally { await client.close(); await server.close(); } }); it("tools/call rejects invalid GraphQL Names in object/operationName", async () => { const { client, server } = await connect(); try { for (const args of [ { org: ORG, object: "1Bad" }, { org: ORG, object: "Account", operationName: "1Bad" }, ]) { const result = await client.callTool({ name: "sf_gql_update", arguments: args }); expect(result.isError).toBe(true); const content = result.content as { type: string; text?: string }[]; expect(content[0]?.text ?? "").toMatch(/GraphQL Name/); } } finally { await client.close(); await server.close(); } }); });