/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import path from "node:path"; 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, introspectionFromSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { atomicWriteJson } from "../../../lib/fs-utils.js"; import { schemaCacheKeyForInstanceUrl, schemaDir, type SchemaMetadata, } from "../../../lib/introspect.js"; import { type PrimeDeps } from "../../../lib/prime-schema.js"; import { primeSchemaCache } from "../../../lib/walker.js"; import { createServer } from "../../server.js"; import { registerSfGqlRawTool } from "../sf-gql-raw.js"; const ORG = "test-tool-raw"; const ORG_URL = "https://test-tool-raw.my.salesforce.com"; const SCHEMA = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { Case(first: Int, after: String, where: Case_Filter): CaseConnection! } input Case_Filter { Status: PicklistOperators } input PicklistOperators { eq: String } type CaseConnection { edges: [CaseEdge!]!, pageInfo: PageInfo! } type CaseEdge { node: Case! } type PageInfo { hasNextPage: Boolean!, endCursor: String } type Case { Id: ID!, Subject: StringValue, Status: StringValue } type StringValue { value: String } `); primeSchemaCache(ORG, SCHEMA); primeSchemaCache(ORG_URL, SCHEMA); const primeDeps: PrimeDeps = { getOrgAuth: async () => ({ alias: ORG, username: "u", instanceUrl: ORG_URL, accessToken: "t", orgId: "00D", }), downloadSchema: async (auth) => { const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl); const filePath = path.join(schemaDir(), `${cacheKey}.json`); atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) }); const meta: SchemaMetadata = { cacheKey, instanceUrl: auth.instanceUrl, typeCount: 0, downloadedAt: new Date().toISOString(), filePath, }; return meta; }, }; async function connect(): Promise<{ client: Client; server: McpServer }> { const server = new McpServer({ name: "graphiti-mcp", version: "test" }); registerSfGqlRawTool(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-raw", () => { it("tools/list advertises sf_gql_raw with org/commands properties", async () => { const { client, server } = await connect(); try { const list = await client.listTools(); const tool = list.tools.find((t) => t.name === "sf_gql_raw"); expect(tool).toBeDefined(); const props = (tool!.inputSchema as { properties?: Record }).properties ?? {}; expect(props.org).toBeDefined(); expect(props.commands).toBeDefined(); // W-22899736: typeName advertises the GraphQL Name pattern in tools/list. expect((props.typeName as { pattern?: string }).pattern).toBe("^[A-Za-z_][A-Za-z0-9_]*$"); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_raw renders a query from commands", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_raw", arguments: { org: ORG, commands: ["select uiapi/query/Case/edges/node/Subject/value"], }, }); expect(result.isError).toBeFalsy(); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string }; expect(parsed.query).toMatch(/Subject\s+@optional\s*\{\s*value\s*\}/); } finally { await client.close(); await server.close(); } }); it("tools/call sf_gql_raw fails fast on a bad command", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_raw", arguments: { org: ORG, commands: ["bogus verb here"] }, }); expect(result.isError).toBe(true); const content = result.content as { type: string; text?: string }[]; expect(content[0]?.text).toMatch(/command 0 \(bogus verb here\)/); } finally { await client.close(); await server.close(); } }); it("tools/call rejects invalid GraphQL Names in typeName", async () => { const { client, server } = await connect(); try { for (const args of [ { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"], typeName: "1Bad" }, ]) { const result = await client.callTool({ name: "sf_gql_raw", 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(); } }); }); describe("mcp/server registers sf_gql_raw", () => { it("createServer advertises sf_gql_raw in tools/list", async () => { const server = createServer(); const [c, s] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test", version: "0.0.0" }); await Promise.all([server.connect(s), client.connect(c)]); try { const list = await client.listTools(); expect(list.tools.find((t) => t.name === "sf_gql_raw")).toBeDefined(); } finally { await client.close(); await server.close(); } }); }); // W-23336443: the OPTIONAL `operation` enum is wrapped in enumStripControlChars. // A z.enum rejection is reflected verbatim by the MCP SDK's input validation, // upstream of runTool. These exercise the real client.callTool channel; the // oracle asserts the specific injected code points (U+202E/U+200B/U+007F) are // absent, not a whole-class scan (the SDK's JSON.stringify(issues, 2) inserts // \p{Cc} newlines that would false-positive). See sf-gql-discover.spec.ts. describe("mcp/tools/sf-gql-raw — control-char neutralization (W-23336443)", () => { const BIDI = "\u{202e}"; const ZWSP = "\u{200b}"; const DEL = "\x7f"; it("a poisoned-but-otherwise-valid operation strips to the member and is ACCEPTED", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_raw", arguments: { org: ORG, commands: ["select uiapi/query/Case/edges/node/Subject/value"], operation: `query${BIDI}`, }, }); // "query" is the default root anyway, so a strip-to-"query" renders the // same query a clean call would — the value passed validation cleanly. expect(result.isError).toBeFalsy(); const content = result.content as { type: string; text?: string }[]; const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string }; expect(parsed.query).toMatch(/Subject\s+@optional\s*\{\s*value\s*\}/); } finally { await client.close(); await server.close(); } }); it("a genuinely-invalid poisoned operation rejects with NO raw injected code point", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_raw", arguments: { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"], operation: `${BIDI}bo${ZWSP}gus${DEL}`, }, }); expect(result.isError).toBe(true); const text = (result.content as { text?: string }[])[0]?.text ?? ""; expect(text).not.toContain(BIDI); expect(text).not.toContain(ZWSP); expect(text).not.toContain(DEL); expect(text).toContain("bogus"); } finally { await client.close(); await server.close(); } }); it("omitting operation still validates (the wrapper preserves .optional())", async () => { const { client, server } = await connect(); try { const result = await client.callTool({ name: "sf_gql_raw", arguments: { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"] }, }); expect(result.isError).toBeFalsy(); } finally { await client.close(); await server.close(); } }); it("tools/list advertises the operation enum and keeps it OPTIONAL", async () => { const { client, server } = await connect(); try { const list = await client.listTools(); const tool = list.tools.find((t) => t.name === "sf_gql_raw"); const schema = tool!.inputSchema as { properties: Record; required?: string[]; }; const op = schema.properties.operation as { enum?: string[]; description?: string }; expect(op.enum).toEqual(["query", "mutation", "aggregate"]); expect(op.description).toMatch(/Operation root/); expect(schema.required ?? []).not.toContain("operation"); } finally { await client.close(); await server.close(); } }); });