@salesforce/graphiti 10.20.1 → 10.21.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.
- package/CHANGELOG.md +11 -0
- package/dist/commands/args.js +15 -2
- package/dist/commands/args.js.map +1 -1
- package/dist/intent/build-aggregate.js +6 -2
- package/dist/intent/build-aggregate.js.map +1 -1
- package/dist/intent/build-detail.js +3 -2
- package/dist/intent/build-detail.js.map +1 -1
- package/dist/intent/build-list.js +11 -7
- package/dist/intent/build-list.js.map +1 -1
- package/dist/intent/select-child-relationship.d.ts +1 -1
- package/dist/intent/select-child-relationship.js +3 -3
- package/dist/intent/select-child-relationship.js.map +1 -1
- package/dist/lib/auth.js +3 -2
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/errors.d.ts +38 -0
- package/dist/lib/errors.js +42 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/introspect.js +4 -3
- package/dist/lib/introspect.js.map +1 -1
- package/dist/lib/prime-schema.js +13 -5
- package/dist/lib/prime-schema.js.map +1 -1
- package/dist/lib/query-builder.js +19 -6
- package/dist/lib/query-builder.js.map +1 -1
- package/dist/lib/session.d.ts +10 -1
- package/dist/lib/session.js +9 -2
- package/dist/lib/session.js.map +1 -1
- package/dist/lib/variable-promotion.js +7 -3
- package/dist/lib/variable-promotion.js.map +1 -1
- package/dist/lib/walker.js +8 -6
- package/dist/lib/walker.js.map +1 -1
- package/dist/mcp/tools/sf-gql-aggregate.js +2 -6
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -1
- package/dist/mcp/tools/sf-gql-connect.js +3 -7
- package/dist/mcp/tools/sf-gql-connect.js.map +1 -1
- package/dist/mcp/tools/sf-gql-create.js +2 -6
- package/dist/mcp/tools/sf-gql-create.js.map +1 -1
- package/dist/mcp/tools/sf-gql-delete.js +2 -6
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -1
- package/dist/mcp/tools/sf-gql-detail.js +2 -6
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -1
- package/dist/mcp/tools/sf-gql-discover.js +2 -6
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -1
- package/dist/mcp/tools/sf-gql-list.js +2 -6
- package/dist/mcp/tools/sf-gql-list.js.map +1 -1
- package/dist/mcp/tools/sf-gql-raw.js +2 -6
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -1
- package/dist/mcp/tools/sf-gql-update.js +2 -6
- package/dist/mcp/tools/sf-gql-update.js.map +1 -1
- package/dist/schemas/tool-adapter.d.ts +56 -0
- package/dist/schemas/tool-adapter.js +129 -0
- package/dist/schemas/tool-adapter.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/args.ts +23 -2
- package/src/intent/__tests__/build-aggregate.spec.ts +64 -0
- package/src/intent/__tests__/build-list.spec.ts +115 -2
- package/src/intent/build-aggregate.ts +7 -3
- package/src/intent/build-detail.ts +4 -2
- package/src/intent/build-list.ts +13 -8
- package/src/intent/select-child-relationship.ts +3 -2
- package/src/lib/__tests__/apply-command.spec.ts +17 -1
- package/src/lib/__tests__/query-builder.spec.ts +68 -0
- package/src/lib/__tests__/session.spec.ts +44 -0
- package/src/lib/__tests__/variable-promotion.spec.ts +58 -0
- package/src/lib/auth.ts +4 -2
- package/src/lib/errors.ts +45 -0
- package/src/lib/introspect.ts +6 -3
- package/src/lib/prime-schema.ts +12 -4
- package/src/lib/query-builder.ts +19 -6
- package/src/lib/session.ts +20 -3
- package/src/lib/variable-promotion.ts +9 -3
- package/src/lib/walker.ts +8 -6
- package/src/mcp/tools/__tests__/error-surface.contract.spec.ts +261 -0
- package/src/mcp/tools/sf-gql-aggregate.ts +2 -6
- package/src/mcp/tools/sf-gql-connect.ts +3 -7
- package/src/mcp/tools/sf-gql-create.ts +2 -6
- package/src/mcp/tools/sf-gql-delete.ts +2 -6
- package/src/mcp/tools/sf-gql-detail.ts +2 -6
- package/src/mcp/tools/sf-gql-discover.ts +2 -6
- package/src/mcp/tools/sf-gql-list.ts +2 -6
- package/src/mcp/tools/sf-gql-raw.ts +2 -6
- package/src/mcp/tools/sf-gql-update.ts +2 -6
- package/src/schemas/__tests__/tool-adapter.spec.ts +299 -0
- package/src/schemas/tool-adapter.ts +165 -0
package/src/lib/walker.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type GraphQLOutputType,
|
|
29
29
|
type GraphQLType,
|
|
30
30
|
} from "graphql";
|
|
31
|
+
import { SchemaError, UserInputError } from "./errors.js";
|
|
31
32
|
import { atomicWriteText } from "./fs-utils.js";
|
|
32
33
|
import { loadIntrospectionResult, getSchemaFilePath, normalizeInstanceUrl } from "./introspect.js";
|
|
33
34
|
import { type OperationType } from "./session.js";
|
|
@@ -291,13 +292,14 @@ function validateFragmentTarget(
|
|
|
291
292
|
): GraphQLNamedType {
|
|
292
293
|
const namedType = schema.getType(targetTypeName);
|
|
293
294
|
if (!namedType) {
|
|
294
|
-
|
|
295
|
+
// User named an inline-fragment target type that does not exist.
|
|
296
|
+
throw new UserInputError(`Type "${targetTypeName}" not found in schema`);
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
if (isUnionType(currentType)) {
|
|
298
300
|
const allowed = currentType.getTypes().map((type) => type.name);
|
|
299
301
|
if (!allowed.includes(targetTypeName)) {
|
|
300
|
-
throw new
|
|
302
|
+
throw new UserInputError(
|
|
301
303
|
`Type "${targetTypeName}" is not a possible type of union ${currentType.name}. Allowed: ${allowed.join(", ")}`,
|
|
302
304
|
);
|
|
303
305
|
}
|
|
@@ -307,7 +309,7 @@ function validateFragmentTarget(
|
|
|
307
309
|
if (isInterfaceType(currentType)) {
|
|
308
310
|
const allowed = schema.getPossibleTypes(currentType).map((type) => type.name);
|
|
309
311
|
if (!allowed.includes(targetTypeName)) {
|
|
310
|
-
throw new
|
|
312
|
+
throw new UserInputError(
|
|
311
313
|
`Type "${targetTypeName}" does not implement interface ${currentType.name}. Allowed: ${allowed.join(", ")}`,
|
|
312
314
|
);
|
|
313
315
|
}
|
|
@@ -338,12 +340,12 @@ function validateFragmentTarget(
|
|
|
338
340
|
if (hints.length > 0) {
|
|
339
341
|
msg += `\nDid you mean to use it through a relationship? Try: ${hints.join(" or ")}`;
|
|
340
342
|
}
|
|
341
|
-
throw new
|
|
343
|
+
throw new UserInputError(msg);
|
|
342
344
|
}
|
|
343
345
|
return namedType;
|
|
344
346
|
}
|
|
345
347
|
|
|
346
|
-
throw new
|
|
348
|
+
throw new UserInputError(
|
|
347
349
|
`Cannot apply an inline fragment at ${currentType.name} (${getTypeKind(currentType)}).`,
|
|
348
350
|
);
|
|
349
351
|
}
|
|
@@ -472,7 +474,7 @@ export function getRootType(schema: GraphQLSchema, operation: OperationType): Gr
|
|
|
472
474
|
const rootType = operation === "mutation" ? schema.getMutationType() : schema.getQueryType();
|
|
473
475
|
|
|
474
476
|
if (!rootType) {
|
|
475
|
-
throw new
|
|
477
|
+
throw new SchemaError(`Schema has no ${operation} type`);
|
|
476
478
|
}
|
|
477
479
|
return rootType;
|
|
478
480
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
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 fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { buildSchema } from "graphql";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import { makeNoopPrimeDeps } from "../../../__tests__/helpers/prime-deps.js";
|
|
16
|
+
import { AuthError } from "../../../lib/errors.js";
|
|
17
|
+
import { type PrimeDeps } from "../../../lib/prime-schema.js";
|
|
18
|
+
import { primeSchemaCache } from "../../../lib/walker.js";
|
|
19
|
+
import { registerSfGqlAggregateTool } from "../sf-gql-aggregate.js";
|
|
20
|
+
import { registerSfGqlConnectTool } from "../sf-gql-connect.js";
|
|
21
|
+
import { registerSfGqlCreateTool } from "../sf-gql-create.js";
|
|
22
|
+
import { registerSfGqlDeleteTool } from "../sf-gql-delete.js";
|
|
23
|
+
import { registerSfGqlDetailTool } from "../sf-gql-detail.js";
|
|
24
|
+
import { registerSfGqlDiscoverTool } from "../sf-gql-discover.js";
|
|
25
|
+
import { registerSfGqlListTool } from "../sf-gql-list.js";
|
|
26
|
+
import { registerSfGqlRawTool } from "../sf-gql-raw.js";
|
|
27
|
+
import { registerSfGqlUpdateTool } from "../sf-gql-update.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* W-22697673 — contract tests for the shared tool adapter. These drive each
|
|
31
|
+
* error category end-to-end through a registered MCP tool (sf_gql_list) so we
|
|
32
|
+
* assert the category prefix is what an MCP host actually receives, not just
|
|
33
|
+
* what `classifyError` returns in isolation.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const ORG = "test-err-surface";
|
|
37
|
+
const ORG_URL = "https://test-err-surface.my.salesforce.com";
|
|
38
|
+
const SCHEMA = buildSchema(`
|
|
39
|
+
type Query { uiapi: UIAPI! }
|
|
40
|
+
type UIAPI { query: RecordQuery! }
|
|
41
|
+
type RecordQuery { Account(first: Int, after: String): AccountConnection! }
|
|
42
|
+
type AccountConnection { edges: [AccountEdge!]!, pageInfo: PageInfo! }
|
|
43
|
+
type AccountEdge { node: Account! }
|
|
44
|
+
type PageInfo { hasNextPage: Boolean!, endCursor: String }
|
|
45
|
+
type Account { Id: ID!, Name: StringValue }
|
|
46
|
+
type StringValue { value: String }
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
async function connectWith(primeDeps: PrimeDeps): Promise<{ client: Client; server: McpServer }> {
|
|
50
|
+
const server = new McpServer({ name: "graphiti-mcp", version: "test" });
|
|
51
|
+
registerSfGqlListTool(server, { primeDeps });
|
|
52
|
+
const [c, s] = InMemoryTransport.createLinkedPair();
|
|
53
|
+
const client = new Client({ name: "test", version: "0.0.0" });
|
|
54
|
+
await Promise.all([server.connect(s), client.connect(c)]);
|
|
55
|
+
return { client, server };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function errorText(result: Awaited<ReturnType<Client["callTool"]>>): string {
|
|
59
|
+
const content = result.content as { type: string; text?: string }[];
|
|
60
|
+
return content[0]?.text ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("mcp/tools error surface — category prefixes (contract)", () => {
|
|
64
|
+
let tmpRoot: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-err-surface-"));
|
|
68
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
69
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
70
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
delete process.env.GRAPHITI_HOME;
|
|
75
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
76
|
+
vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("UserInput: a malformed sf_gql_raw command surfaces with the UserInput prefix", async () => {
|
|
80
|
+
// `commands` are free-form strings (no GraphQL-Name zod gate), so a bad verb
|
|
81
|
+
// passes input validation and reaches apply-command — through runTool — which
|
|
82
|
+
// throws "unknown command". This is a UserInput error the SDK does not pre-empt.
|
|
83
|
+
const server = new McpServer({ name: "graphiti-mcp", version: "test" });
|
|
84
|
+
registerSfGqlRawTool(server, { primeDeps: makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA) });
|
|
85
|
+
const [c, s] = InMemoryTransport.createLinkedPair();
|
|
86
|
+
const client = new Client({ name: "test", version: "0.0.0" });
|
|
87
|
+
await Promise.all([server.connect(s), client.connect(c)]);
|
|
88
|
+
try {
|
|
89
|
+
const result = await client.callTool({
|
|
90
|
+
name: "sf_gql_raw",
|
|
91
|
+
arguments: { org: ORG, commands: ["bogusverb uiapi/query/Account"] },
|
|
92
|
+
});
|
|
93
|
+
expect(result.isError).toBe(true);
|
|
94
|
+
expect(errorText(result)).toMatch(/^UserInput: /);
|
|
95
|
+
expect(errorText(result)).toMatch(/unknown command/);
|
|
96
|
+
} finally {
|
|
97
|
+
await client.close();
|
|
98
|
+
await server.close();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("Auth: a credential failure surfaces with the Auth prefix", async () => {
|
|
103
|
+
const primeDeps: PrimeDeps = {
|
|
104
|
+
...makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA),
|
|
105
|
+
getOrgAuth: async () => {
|
|
106
|
+
throw new AuthError('Failed to get org info for "test-err-surface"');
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const { client, server } = await connectWith(primeDeps);
|
|
110
|
+
try {
|
|
111
|
+
const result = await client.callTool({
|
|
112
|
+
name: "sf_gql_list",
|
|
113
|
+
arguments: { org: ORG, object: "Account", fields: ["Id"] },
|
|
114
|
+
});
|
|
115
|
+
expect(result.isError).toBe(true);
|
|
116
|
+
expect(errorText(result)).toMatch(/^Auth: /);
|
|
117
|
+
} finally {
|
|
118
|
+
await client.close();
|
|
119
|
+
await server.close();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("Schema: an introspection/priming failure surfaces with the Schema prefix", async () => {
|
|
124
|
+
// No prior disk cache (fresh GRAPHITI_HOME) + a download that throws an
|
|
125
|
+
// untyped error → primeSchemaWithLock wraps it as SchemaError.
|
|
126
|
+
const primeDeps: PrimeDeps = {
|
|
127
|
+
...makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA),
|
|
128
|
+
downloadSchema: async () => {
|
|
129
|
+
throw new Error("connection.request failed: socket hang up");
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const { client, server } = await connectWith(primeDeps);
|
|
133
|
+
try {
|
|
134
|
+
const result = await client.callTool({
|
|
135
|
+
name: "sf_gql_list",
|
|
136
|
+
arguments: { org: ORG, object: "Account", fields: ["Id"] },
|
|
137
|
+
});
|
|
138
|
+
expect(result.isError).toBe(true);
|
|
139
|
+
expect(errorText(result)).toMatch(/^Schema: /);
|
|
140
|
+
} finally {
|
|
141
|
+
await client.close();
|
|
142
|
+
await server.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("Internal: an unexpected error is sanitized, prefixed, and logged to stderr", async () => {
|
|
147
|
+
const stderr = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
148
|
+
const home = os.homedir();
|
|
149
|
+
const primeDeps: PrimeDeps = {
|
|
150
|
+
...makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA),
|
|
151
|
+
// A throw from getOrgAuth (outside the lock) propagates verbatim, so an
|
|
152
|
+
// unrecognized message lands in the Internal bucket rather than Schema.
|
|
153
|
+
getOrgAuth: async () => {
|
|
154
|
+
throw new Error(`unexpected boom at ${home}/private/creds.ts`);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const { client, server } = await connectWith(primeDeps);
|
|
158
|
+
try {
|
|
159
|
+
const result = await client.callTool({
|
|
160
|
+
name: "sf_gql_list",
|
|
161
|
+
arguments: { org: ORG, object: "Account", fields: ["Id"] },
|
|
162
|
+
});
|
|
163
|
+
expect(result.isError).toBe(true);
|
|
164
|
+
const text = errorText(result);
|
|
165
|
+
expect(text).toMatch(/^Internal: /);
|
|
166
|
+
// The host never sees the developer's home directory.
|
|
167
|
+
expect(text).not.toContain(home);
|
|
168
|
+
// The full error is still logged off the stdio channel for operators.
|
|
169
|
+
expect(stderr).toHaveBeenCalled();
|
|
170
|
+
} finally {
|
|
171
|
+
await client.close();
|
|
172
|
+
await server.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* M7 — every tool, not just sf_gql_list, must route through `runTool`. The
|
|
179
|
+
* `runTool(() => buildX(...))` one-liner is hand-repeated in 9 files; a
|
|
180
|
+
* copy-paste miss (omitting runTool) would still yield `isError` from the SDK
|
|
181
|
+
* but WITHOUT the `<Category>: ` prefix — and the pre-existing per-tool specs,
|
|
182
|
+
* which assert only `isError`/substrings, would not catch it. This table injects
|
|
183
|
+
* a throwing `getOrgAuth` (the first call in priming, reached by every tool) and
|
|
184
|
+
* asserts the host envelope carries the `Auth: ` prefix from all 9 tools.
|
|
185
|
+
*/
|
|
186
|
+
describe("mcp/tools error surface — all 9 tools route through runTool (M7)", () => {
|
|
187
|
+
let tmpRoot: string;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-err-surface-all-"));
|
|
191
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
delete process.env.GRAPHITI_HOME;
|
|
196
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const authFailingDeps = (): PrimeDeps => ({
|
|
200
|
+
...makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA),
|
|
201
|
+
getOrgAuth: async () => {
|
|
202
|
+
throw new AuthError('Failed to get org info for "test-err-surface"');
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const TOOLS: {
|
|
207
|
+
name: string;
|
|
208
|
+
register: (server: McpServer, opts: { primeDeps?: PrimeDeps }) => void;
|
|
209
|
+
args: Record<string, unknown>;
|
|
210
|
+
}[] = [
|
|
211
|
+
{
|
|
212
|
+
name: "sf_gql_list",
|
|
213
|
+
register: registerSfGqlListTool,
|
|
214
|
+
args: { object: "Account", fields: ["Id"] },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "sf_gql_detail",
|
|
218
|
+
register: registerSfGqlDetailTool,
|
|
219
|
+
args: { object: "Account", fields: ["Id"] },
|
|
220
|
+
},
|
|
221
|
+
{ name: "sf_gql_aggregate", register: registerSfGqlAggregateTool, args: { object: "Account" } },
|
|
222
|
+
{ name: "sf_gql_create", register: registerSfGqlCreateTool, args: { object: "Account" } },
|
|
223
|
+
{ name: "sf_gql_update", register: registerSfGqlUpdateTool, args: { object: "Account" } },
|
|
224
|
+
{ name: "sf_gql_delete", register: registerSfGqlDeleteTool, args: { object: "Account" } },
|
|
225
|
+
{
|
|
226
|
+
name: "sf_gql_raw",
|
|
227
|
+
register: registerSfGqlRawTool,
|
|
228
|
+
args: { commands: ["select uiapi/query/Account/edges/node/Id"] },
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "sf_gql_discover",
|
|
232
|
+
register: registerSfGqlDiscoverTool,
|
|
233
|
+
args: { mode: "list_objects" },
|
|
234
|
+
},
|
|
235
|
+
{ name: "sf_gql_connect", register: registerSfGqlConnectTool, args: {} },
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const tool of TOOLS) {
|
|
239
|
+
it(`${tool.name} surfaces a category-prefixed envelope (not a raw SDK error)`, async () => {
|
|
240
|
+
const server = new McpServer({ name: "graphiti-mcp", version: "test" });
|
|
241
|
+
tool.register(server, { primeDeps: authFailingDeps() });
|
|
242
|
+
const [c, s] = InMemoryTransport.createLinkedPair();
|
|
243
|
+
const client = new Client({ name: "test", version: "0.0.0" });
|
|
244
|
+
await Promise.all([server.connect(s), client.connect(c)]);
|
|
245
|
+
try {
|
|
246
|
+
const result = await client.callTool({
|
|
247
|
+
name: tool.name,
|
|
248
|
+
arguments: { org: ORG, ...tool.args },
|
|
249
|
+
});
|
|
250
|
+
expect(result.isError).toBe(true);
|
|
251
|
+
// The prefix is proof the handler went through runTool: the SDK's own
|
|
252
|
+
// thrown-handler path would emit the bare message with no category.
|
|
253
|
+
expect(errorText(result)).toMatch(/^(UserInput|Auth|Schema|Internal): /);
|
|
254
|
+
expect(errorText(result)).toMatch(/^Auth: /);
|
|
255
|
+
} finally {
|
|
256
|
+
await client.close();
|
|
257
|
+
await server.close();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildAggregate } from "../../intent/build-aggregate.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { AGGREGATE_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlAggregateToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -26,11 +27,6 @@ export function registerSfGqlAggregateTool(
|
|
|
26
27
|
"Build a UIAPI aggregate query against a Salesforce org. Supports count/countDistinct/sum/avg/min/max with optional groupBy and filter. Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings.",
|
|
27
28
|
inputSchema,
|
|
28
29
|
},
|
|
29
|
-
async (args) =>
|
|
30
|
-
const output = await buildAggregate(args, opts.primeDeps);
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
|
-
};
|
|
34
|
-
},
|
|
30
|
+
async (args) => runTool(() => buildAggregate(args, opts.primeDeps)),
|
|
35
31
|
);
|
|
36
32
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { buildConnect, type ConnectDeps } from "../../intent/build-connect.js";
|
|
9
9
|
import { CONNECT_INPUT } from "../../schemas/input-schemas.js";
|
|
10
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
10
11
|
|
|
11
12
|
export type SfGqlConnectToolOptions = ConnectDeps;
|
|
12
13
|
|
|
@@ -20,14 +21,9 @@ export function registerSfGqlConnectTool(
|
|
|
20
21
|
"sf_gql_connect",
|
|
21
22
|
{
|
|
22
23
|
description:
|
|
23
|
-
"Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring.",
|
|
24
|
+
"Connect to a Salesforce org and prime its GraphQL schema cache. With forceRefresh, re-download the schema and coherently clear all caches so subsequent tools see freshly-deployed metadata. Concurrent refreshes coalesce into a single introspection. Returns { org, instanceUrl, refreshed, cached, durationMs, warnings? } — not the standard ToolOutput envelope. If a refresh fails but a usable cached schema survives, returns refreshed:false with a staleness warning instead of erroring. Error convention (all sf_gql_* tools): on failure the isError text is prefixed with a category — `UserInput:` (fix the request), `Auth:` (re-authenticate the org), `Schema:` (introspection/cache problem), or `Internal:` (unexpected) — so you can decide whether to fix inputs, re-auth, or retry.",
|
|
24
25
|
inputSchema,
|
|
25
26
|
},
|
|
26
|
-
async (args) =>
|
|
27
|
-
const output = await buildConnect(args, opts);
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
30
|
-
};
|
|
31
|
-
},
|
|
27
|
+
async (args) => runTool(() => buildConnect(args, opts)),
|
|
32
28
|
);
|
|
33
29
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildCreate } from "../../intent/build-create.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { CREATE_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlCreateToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -26,11 +27,6 @@ export function registerSfGqlCreateTool(
|
|
|
26
27
|
"Build a UIAPI create mutation against a Salesforce org. Declares a typed input variable ($input: <Object>CreateInput!) and selects return fields on the created record. Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings.",
|
|
27
28
|
inputSchema,
|
|
28
29
|
},
|
|
29
|
-
async (args) =>
|
|
30
|
-
const output = await buildCreate(args, opts.primeDeps);
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
|
-
};
|
|
34
|
-
},
|
|
30
|
+
async (args) => runTool(() => buildCreate(args, opts.primeDeps)),
|
|
35
31
|
);
|
|
36
32
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildDelete } from "../../intent/build-delete.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { DELETE_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlDeleteToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -26,11 +27,6 @@ export function registerSfGqlDeleteTool(
|
|
|
26
27
|
"Build a UIAPI delete mutation against a Salesforce org. Declares a typed input variable ($input: RecordDeleteInput!) and selects Id on the deleted record. The input type is schema-wide (not <Object>-specific) and the result is Id only. Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings.",
|
|
27
28
|
inputSchema,
|
|
28
29
|
},
|
|
29
|
-
async (args) =>
|
|
30
|
-
const output = await buildDelete(args, opts.primeDeps);
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
|
-
};
|
|
34
|
-
},
|
|
30
|
+
async (args) => runTool(() => buildDelete(args, opts.primeDeps)),
|
|
35
31
|
);
|
|
36
32
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildDetail } from "../../intent/build-detail.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { DETAIL_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlDetailToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -35,11 +36,6 @@ export function registerSfGqlDetailTool(
|
|
|
35
36
|
].join("\n"),
|
|
36
37
|
inputSchema,
|
|
37
38
|
},
|
|
38
|
-
async (args) =>
|
|
39
|
-
const output = await buildDetail(args, opts.primeDeps);
|
|
40
|
-
return {
|
|
41
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
42
|
-
};
|
|
43
|
-
},
|
|
39
|
+
async (args) => runTool(() => buildDetail(args, opts.primeDeps)),
|
|
44
40
|
);
|
|
45
41
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { buildDiscover, type DiscoverDeps } from "../../intent/build-discover.js";
|
|
9
9
|
import { DISCOVER_INPUT } from "../../schemas/input-schemas.js";
|
|
10
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
10
11
|
|
|
11
12
|
export type SfGqlDiscoverToolOptions = DiscoverDeps;
|
|
12
13
|
|
|
@@ -23,11 +24,6 @@ export function registerSfGqlDiscoverTool(
|
|
|
23
24
|
"Discover Salesforce UIAPI schema metadata: list queryable SObjects, describe an object's fields/picklists/child relationships, or describe a single field. Returns mode-specific metadata (not the standard ToolOutput envelope).",
|
|
24
25
|
inputSchema,
|
|
25
26
|
},
|
|
26
|
-
async (args) =>
|
|
27
|
-
const output = await buildDiscover(args, opts);
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
30
|
-
};
|
|
31
|
-
},
|
|
27
|
+
async (args) => runTool(() => buildDiscover(args, opts)),
|
|
32
28
|
);
|
|
33
29
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildList } from "../../intent/build-list.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { LIST_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlListToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -23,11 +24,6 @@ export function registerSfGqlListTool(server: McpServer, opts: SfGqlListToolOpti
|
|
|
23
24
|
"Build a UIAPI list query against a Salesforce org. Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings.",
|
|
24
25
|
inputSchema,
|
|
25
26
|
},
|
|
26
|
-
async (args) =>
|
|
27
|
-
const output = await buildList(args, opts.primeDeps);
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
30
|
-
};
|
|
31
|
-
},
|
|
27
|
+
async (args) => runTool(() => buildList(args, opts.primeDeps)),
|
|
32
28
|
);
|
|
33
29
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildRaw } from "../../intent/build-raw.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { RAW_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlRawToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -23,11 +24,6 @@ export function registerSfGqlRawTool(server: McpServer, opts: SfGqlRawToolOption
|
|
|
23
24
|
"Low-level escape hatch: build an arbitrary UIAPI query, mutation, or aggregate from CLI-style commands (select/set/var) when the typed tools don't model the case (cross-union selections, custom mutations). Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings. Fails fast on a bad command.",
|
|
24
25
|
inputSchema,
|
|
25
26
|
},
|
|
26
|
-
async (args) =>
|
|
27
|
-
const output = await buildRaw(args, opts.primeDeps);
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
30
|
-
};
|
|
31
|
-
},
|
|
27
|
+
async (args) => runTool(() => buildRaw(args, opts.primeDeps)),
|
|
32
28
|
);
|
|
33
29
|
}
|
|
@@ -8,6 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { buildUpdate } from "../../intent/build-update.js";
|
|
9
9
|
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
10
10
|
import { UPDATE_INPUT } from "../../schemas/input-schemas.js";
|
|
11
|
+
import { runTool } from "../../schemas/tool-adapter.js";
|
|
11
12
|
|
|
12
13
|
export interface SfGqlUpdateToolOptions {
|
|
13
14
|
primeDeps?: PrimeDeps;
|
|
@@ -26,11 +27,6 @@ export function registerSfGqlUpdateTool(
|
|
|
26
27
|
"Build a UIAPI update mutation against a Salesforce org. Declares a typed input variable ($input: <Object>UpdateInput!) and selects return fields on the updated record. Returns rendered GraphQL, declared variables, generated TypeScript types, and validation warnings.",
|
|
27
28
|
inputSchema,
|
|
28
29
|
},
|
|
29
|
-
async (args) =>
|
|
30
|
-
const output = await buildUpdate(args, opts.primeDeps);
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
|
-
};
|
|
34
|
-
},
|
|
30
|
+
async (args) => runTool(() => buildUpdate(args, opts.primeDeps)),
|
|
35
31
|
);
|
|
36
32
|
}
|