Skip to content

Commit 701faa0

Browse files
github-actions[bot]agentfront[bot]frontegg-david
authored
Cherry-pick: fix: update elicitation support logic to align with MCP spec and enhance type handling (Fixes #279) (#295)
Cherry-picked from #294 (merged to release/1.0.x) Original commit: c1cf59f Co-authored-by: agentfront[bot] <agentfront[bot]@users.noreply.github.com> Co-authored-by: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
1 parent dde0d83 commit 701faa0

3 files changed

Lines changed: 43 additions & 11 deletions

File tree

libs/sdk/src/notification/__tests__/elicitation-support.spec.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ describe('supportsElicitation', () => {
2121
expect(supportsElicitation(capabilities)).toBe(false);
2222
});
2323

24-
it('should return false when elicitation is empty object', () => {
24+
it('should return true when elicitation is empty object (per MCP spec 2025-06-18)', () => {
2525
const capabilities: ClientCapabilities = {
2626
elicitation: {},
2727
};
28-
expect(supportsElicitation(capabilities)).toBe(false);
28+
// Per MCP spec: presence of elicitation object means client supports it
29+
expect(supportsElicitation(capabilities)).toBe(true);
30+
});
31+
32+
it('should return false when checking specific mode on empty elicitation', () => {
33+
const capabilities: ClientCapabilities = {
34+
elicitation: {},
35+
};
36+
expect(supportsElicitation(capabilities, 'form')).toBe(false);
37+
expect(supportsElicitation(capabilities, 'url')).toBe(false);
2938
});
3039
});
3140

@@ -268,5 +277,16 @@ describe('supportsElicitation', () => {
268277

269278
expect(supportsElicitation(legacyCapabilities)).toBe(false);
270279
});
280+
281+
it('should work with MCP Inspector capabilities (elicitation: {})', () => {
282+
// MCP Inspector sends elicitation: {} per MCP spec 2025-06-18
283+
const inspectorCapabilities: ClientCapabilities = {
284+
sampling: {},
285+
elicitation: {},
286+
roots: { listChanged: true },
287+
};
288+
289+
expect(supportsElicitation(inspectorCapabilities)).toBe(true);
290+
});
271291
});
272292
});

libs/sdk/src/notification/notification.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export function supportsElicitation(capabilities?: ClientCapabilities, mode?: 'f
119119
return false;
120120
}
121121

122+
// Per MCP spec (2025-06-18): presence of the elicitation object means
123+
// the client supports it. Sub-fields (form, url) are optional refinements.
122124
if (mode === 'form') {
123125
return capabilities.elicitation.form !== undefined;
124126
}
@@ -127,8 +129,8 @@ export function supportsElicitation(capabilities?: ClientCapabilities, mode?: 'f
127129
return capabilities.elicitation.url !== undefined;
128130
}
129131

130-
// If no mode specified, check for any elicitation support
131-
return capabilities.elicitation.form !== undefined || capabilities.elicitation.url !== undefined;
132+
// No mode specified → elicitation object exists, client supports it
133+
return true;
132134
}
133135

134136
/**

scripts/generate-schema-types.mjs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ function getZodTypeName(schema) {
9393
* Unwrap layers of ZodOptional, ZodDefault, ZodEffects, ZodNullable
9494
* and collect metadata along the way.
9595
*/
96+
/** Stringify a JS value using single quotes for strings (prettier-compatible). */
97+
function toTsLiteral(value) {
98+
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
99+
return JSON.stringify(value);
100+
}
101+
96102
function unwrapSchema(schema) {
97103
let optional = false;
98104
let defaultValue;
@@ -119,7 +125,11 @@ function unwrapSchema(schema) {
119125
optional = true;
120126
const dv = current._def.defaultValue;
121127
if (typeof dv === 'function') {
122-
try { defaultValue = JSON.stringify(dv()); } catch { /* ignore */ }
128+
try {
129+
defaultValue = JSON.stringify(dv());
130+
} catch {
131+
/* ignore */
132+
}
123133
} else if (dv !== undefined) {
124134
defaultValue = JSON.stringify(dv);
125135
}
@@ -198,19 +208,19 @@ function zodToTsType(schema, depth = 0) {
198208
return 'unknown';
199209

200210
case 'ZodLiteral':
201-
return JSON.stringify(schema._def.value);
211+
return toTsLiteral(schema._def.value);
202212

203213
case 'ZodEnum': {
204214
// Zod v4 uses _def.entries (object), v3 uses _def.values (array)
205215
const entries = schema._def.entries || {};
206216
const vals = Array.isArray(schema._def.values) ? schema._def.values : Object.values(entries);
207-
return vals.map(v => JSON.stringify(v)).join(' | ');
217+
return vals.map((v) => toTsLiteral(v)).join(' | ');
208218
}
209219

210220
case 'ZodNativeEnum': {
211221
const enumObj = schema._def.values || {};
212-
const vals = Object.values(enumObj).filter(v => typeof v === 'string' || typeof v === 'number');
213-
return vals.map(v => JSON.stringify(v)).join(' | ') || 'unknown';
222+
const vals = Object.values(enumObj).filter((v) => typeof v === 'string' || typeof v === 'number');
223+
return vals.map((v) => toTsLiteral(v)).join(' | ') || 'unknown';
214224
}
215225

216226
case 'ZodArray':
@@ -240,7 +250,7 @@ function zodToTsType(schema, depth = 0) {
240250
case 'ZodUnion':
241251
case 'ZodDiscriminatedUnion': {
242252
const options = schema._def.options || [];
243-
return options.map(o => zodToTsType(o, depth)).join(' | ') || 'unknown';
253+
return options.map((o) => zodToTsType(o, depth)).join(' | ') || 'unknown';
244254
}
245255

246256
case 'ZodIntersection': {
@@ -251,7 +261,7 @@ function zodToTsType(schema, depth = 0) {
251261

252262
case 'ZodTuple': {
253263
const items = schema._def.items || [];
254-
return `[${items.map(i => zodToTsType(i, depth)).join(', ')}]`;
264+
return `[${items.map((i) => zodToTsType(i, depth)).join(', ')}]`;
255265
}
256266

257267
case 'ZodOptional':

0 commit comments

Comments
 (0)